Upgrade Professional Edition

This commit is contained in:
kuaifan 2023-03-24 07:17:23 +08:00
parent a172909ddf
commit 3a9001e091
1228 changed files with 108330 additions and 34534 deletions

View File

@ -1,13 +1,14 @@
APP_NAME=DooTask
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_DEBUG=false
APP_SCHEME=auto
APP_URL=http://localhost
APP_ID=
APP_IPPR=
APP_PORT=2222
APP_DEV_PORT=
LOG_CHANNEL=stack
LOG_LEVEL=debug
@ -53,6 +54,9 @@ PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
JUKE_KEY_JOKE=
JUKE_KEY_SOUP=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

View File

@ -1,75 +0,0 @@
name: Build Generic
on:
push:
tags:
- 'v*'
jobs:
build-mac:
runs-on: macos-latest
environment: build
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Create changelog text
id: changelog
uses: loopwerk/tag-changelog@v1
with:
token: ${{ secrets.GH_PAT }}
exclude_types: other,chore,build
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Build for MacOS
env:
APPLEID: ${{ secrets.APPLEID }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.CSC_LINK }}
PROVIDER: "generic"
RELEASE_BODY: ${{ steps.changelog.outputs.changes }}
run: ./cmd electron build-mac
build-win:
runs-on: windows-latest
environment: build
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Create changelog text
id: changelog
uses: loopwerk/tag-changelog@v1
with:
token: ${{ secrets.GH_PAT }}
exclude_types: other,chore,build
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Build for Windows
shell: powershell
env:
PROVIDER: "generic"
RELEASE_BODY: ${{ steps.changelog.outputs.changes }}
run: |
npm install
cd electron
npm install
cd ../
mkdir -p ./electron/public
cp ./electron/index.html ./electron/public/index.html
npx mix --production -- --env --electron
node ./electron/build.js build-win

View File

@ -1,85 +0,0 @@
name: Build Main
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Create changelog text
id: changelog
uses: loopwerk/tag-changelog@v1
with:
token: ${{ secrets.GH_PAT }}
exclude_types: other,chore,build
- name: Create release
uses: actions/create-release@latest
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: ${{ steps.changelog.outputs.changes }}
build-mac:
runs-on: macos-latest
environment: build
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Build for MacOS
env:
APPLEID: ${{ secrets.APPLEID }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.CSC_LINK }}
GH_TOKEN: ${{ secrets.GH_PAT }}
EP_PRE_RELEASE: true
run: ./cmd electron build-mac
build-win:
runs-on: windows-latest
environment: build
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Build for Windows
shell: powershell
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
EP_PRE_RELEASE: true
run: |
npm install
cd electron
npm install
cd ../
mkdir -p ./electron/public
cp ./electron/index.html ./electron/public/index.html
npx mix --production -- --env --electron
node ./electron/build.js build-win

33
.github/workflows/publish-desktop.yml vendored Normal file
View File

@ -0,0 +1,33 @@
name: Publish Desktop
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: macos-latest
environment: build
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Build
env:
APPLEID: ${{ secrets.APPLEID }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
DP_KEY: ${{ secrets.DP_KEY }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_REPOSITORY: ${{ github.repository }}
run: ./cmd electron all

2
.gitignore vendored
View File

@ -5,6 +5,7 @@
/public/.well-known
/public/.user.ini
/storage/*.key
/config/LICENSE
/vendor
/build
/tmp
@ -14,7 +15,6 @@
.vscode
.vagrant
.phpunit.result.cache
CHANGELOG.md
Homestead.json
Homestead.yaml
npm-debug.log

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "resources/drawio"]
path = resources/drawio
url = https://github.com/jgraph/drawio.git
[submodule "resources/mobile"]
path = resources/mobile
url = https://github.com/kuaifan/dootask-app.git

13
.gitpod.yml Normal file
View File

@ -0,0 +1,13 @@
# This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
# and commit this file to your remote git repository to share the goodness with others.
tasks:
- init: sudo ./cmd install
command: ./cmd dev
ports:
- port: 2222
visibility: public
- port: 22222
visibility: public

1959
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,16 @@ cd dootask
./cmd port 2222
```
### Change App Url
```bash
# This URL only affects the email reply.
./cmd url {Your domain url}
# example:
./cmd url https://domain.com
```
### Stop server
```bash
@ -106,7 +116,8 @@ git pull
./cmd mysql recovery
```
If 502 after the upgrade please run `./cmd restart` restart the service.
* Please try again if the upgrade fails across a large version.
* If 502 after the upgrade please run `./cmd restart` restart the service.
## Transfer

21
README_CLIENT.md Normal file
View File

@ -0,0 +1,21 @@
# 客户端说明
## 1、App客户端
#### 1.1、说明
目录 `resources/mobile`,使用`eeui.app`框架遵从eeui的开发文档进行打包开发app
#### 1.2、编译App
1. 在项目目录执行 `./cmd appbuild` 编译
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用
## 2、PC/Mac客户端
#### 2.1、说明
目录 `electron`,使用`electron`框架遵从electron的开发文档进行打包客户端
#### 2.2、编译客户端
在项目目录执行 `./cmd electron` 根据提示编译

View File

@ -45,6 +45,16 @@ cd dootask
./cmd port 2222
```
### 更换URL
```bash
# 此地址仅影响邮件回复功能
./cmd url {域名地址}
# 例如:
./cmd url https://domain.com
```
### 停止服务
```bash
@ -107,7 +117,8 @@ git pull
./cmd mysql recovery
```
如果升级后出现502请运行 `./cmd restart` 重启服务即可。
* 跨越大版本升级失败时请重试执行一次。
* 如果升级后出现502请运行 `./cmd restart` 重启服务即可。
## 迁移项目

26
README_PUBLISH.md Normal file
View File

@ -0,0 +1,26 @@
# 发布说明
## 发布前
1. 添加环境变量 `APPLEID``APPLEIDPASS` 用于公证
2. 添加环境变量 `CSC_LINK``CSC_KEY_PASSWORD` 用于签名
3. 添加环境变量 `GH_TOKEN``GH_REPOSITORY` 用于发布到GitHub
4. 添加环境变量 `DP_KEY` 用于发布到私有服务器
## 通过 GitHub Actions 发布
1. 执行 `npm run version` 生成版本
2. 执行 `npm run build` 编译前端
3. 执行 `git commit` 提交并推送
4. 添加并推送标签
## 本地发布
1. 执行 `npm run version` 生成版本
2. 执行 `npm run build` 编译前端
3. 执行 `./cmd electron` 相关操作
## 编译App
1. 执行 `./cmd appbuild``./cmd appbuild setting` 编译
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用

File diff suppressed because it is too large Load Diff

View File

@ -67,7 +67,13 @@ class Handler extends ExceptionHandler
public function report(Throwable $e)
{
if ($e instanceof ApiException) {
Log::error($e->getMessage(), ['exception' => ' at ' . $e->getFile() .':' . $e->getLine()]);
if ($e->getCode() !== -1) {
Log::error($e->getMessage(), [
'code' => $e->getCode(),
'data' => $e->getData(),
'exception' => ' at ' . $e->getFile() . ':' . $e->getLine()
]);
}
} else {
parent::report($e);
}

View File

@ -15,3 +15,10 @@ if (!function_exists('seeders_at')) {
return date("Y-m-d H:i:s", $time);
}
}
if (!function_exists('md5s')) {
function md5s($val, $len = 16)
{
return substr(md5($val), 32 - $len);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@ use App\Module\Base;
use App\Module\Ihttp;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Redirect;
use Request;
/**
@ -44,11 +45,12 @@ class FileController extends AbstractController
$pid = intval($data['pid']);
//
$permission = 1000;
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
$builder = File::wherePid($pid);
if ($pid > 0) {
File::permissionFind($pid, 0, $permission);
$builder = File::wherePid($pid);
File::permissionFind($pid, $userids, 0, $permission);
} else {
$builder = File::whereUserid($user->userid);
$builder->whereUserid($user->userid);
}
//
$array = $builder->take(500)->get()->toArray();
@ -65,9 +67,25 @@ class FileController extends AbstractController
}
$pid = $file->pid;
$temp = $file->toArray();
$temp['permission'] = $file->getPermission($user->userid);
$temp['permission'] = $file->getPermission($userids);
$array[] = $temp;
}
// 去除没有权限的文件
$isUnset = false;
foreach ($array as $index1 => $item1) {
if ($item1['permission'] === -1) {
foreach ($array as $index2 => $item2) {
if ($item2['pid'] === $item1['id']) {
$array[$index2]['pid'] = 0;
}
}
$isUnset = true;
unset($array[$index1]);
}
}
if ($isUnset) {
$array = array_values($array);
}
} else {
// 获取共享相关
DB::statement("SET SQL_MODE=''");
@ -75,10 +93,7 @@ class FileController extends AbstractController
$list = File::select(["files.*", DB::raw("MAX({$pre}file_users.permission) as permission")])
->join('file_users', 'files.id', '=', 'file_users.file_id')
->where('files.userid', '!=', $user->userid)
->where(function ($query) use ($user) {
$query->where('file_users.userid', 0);
$query->orWhere('file_users.userid', $user->userid);
})
->whereIn('file_users.userid', $userids)
->groupBy('files.id')
->take(100)
->get();
@ -92,7 +107,7 @@ class FileController extends AbstractController
}
// 图片直接返回预览地址
foreach ($array as &$item) {
File::handleImageUrl($item);
$item = File::handleImageUrl($item);
}
return Base::retSuccess('success', $array);
}
@ -119,13 +134,18 @@ class FileController extends AbstractController
//
$permission = 0;
if (Base::isNumber($id)) {
User::auth();
$file = File::permissionFind(intval($id), 0, $permission);
$user = User::auth();
$file = File::permissionFind(intval($id), $user, 0, $permission);
} elseif ($id) {
$fileLink = FileLink::whereCode($id)->first();
$file = $fileLink?->file;
if (empty($file)) {
return Base::retError('链接不存在');
$msg = '文件链接不存在';
$data = File::code2IdName($id);
if ($data) {
$msg = "{$data->name}{$msg}";
}
return Base::retError($msg, $data);
}
} else {
return Base::retError('参数错误');
@ -144,6 +164,7 @@ class FileController extends AbstractController
* @apiGroup file
* @apiName search
*
* @apiParam {String} [link] 通过分享地址搜索https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==
* @apiParam {String} [key] 关键词
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@ -154,15 +175,54 @@ class FileController extends AbstractController
{
$user = User::auth();
//
$link = trim(Request::input('link'));
$key = trim(Request::input('key'));
if (empty($key)) {
return Base::retError('请输入关键词');
$id = 0;
$take = 50;
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
$id = intval(FileLink::whereCode($match[1])->value('file_id'));
$take = 1;
}
// 搜索自己的
$builder = File::whereUserid($user->userid);
if ($id) {
$builder->where("id", $id);
}
if ($key) {
$builder->where("name", "like", "%{$key}%");
}
$array = $builder->take($take)->get()->toArray();
// 搜索共享的
$take = $take - count($array);
if ($take > 0 && ($id || $key)) {
$builder = File::whereIn('pshare', function ($queryA) use ($user) {
$queryA->select('files.id')
->from('files')
->join('file_users', 'files.id', '=', 'file_users.file_id')
->where('files.userid', '!=', $user->userid)
->where(function ($queryB) use ($user) {
$queryB->whereIn('file_users.userid', [0, $user->userid]);
});
});
if ($id) {
$builder->where("id", $id);
}
if ($key) {
$builder->where("name", "like", "%{$key}%");
}
$list = $builder->take($take)->get();
if ($list->isNotEmpty()) {
foreach ($list as $file) {
$temp = $file->toArray();
if ($file->pshare === $file->id) {
$temp['pid'] = 0;
}
$array[] = $temp;
}
}
}
//
$builder = File::whereUserid($user->userid)->where("name", "like", "%{$key}%");
$list = $builder->take(50)->get();
//
return Base::retSuccess('success', $list);
return Base::retSuccess('success', $array);
}
/**
@ -195,15 +255,24 @@ class FileController extends AbstractController
} elseif (mb_strlen($name) > 32) {
return Base::retError('文件名称最多只能设置32个字');
}
$tmpName = preg_replace("/[\\\\\/:*?\"<>|]/", '', $name);
if ($tmpName != $name) {
return Base::retError("文件名称不能包含这些字符:\/:*?\"<>|");
}
//
if ($id > 0) {
// 修改
$file = File::permissionFind($id, 1);
$file = File::permissionFind($id, $user, 1);
//
$file->name = $name;
$file->handleDuplicateName();
$file->save();
$file->pushMsg('update', $file);
return Base::retSuccess('修改成功', $file);
$data = [
'id' => $file->id,
'name' => $file->name,
];
$file->pushMsg('update', $data);
return Base::retSuccess('修改成功', $data);
} else {
// 添加
if (!in_array($type, [
@ -240,7 +309,7 @@ class FileController extends AbstractController
if (File::wherePid($pid)->count() >= 300) {
return Base::retError('每个文件夹里最多只能创建300个文件或文件夹');
}
$row = File::permissionFind($pid, 1);
$row = File::permissionFind($pid, $user, 1);
$userid = $row->userid;
} else {
if (File::whereUserid($user->userid)->wherePid(0)->count() >= 300) {
@ -256,7 +325,8 @@ class FileController extends AbstractController
'userid' => $userid,
'created_id' => $user->userid,
]);
$file->saveBeforePids();
$file->handleDuplicateName();
$file->saveBeforePP();
//
$data = File::find($file->id);
$data->pushMsg('add', $data);
@ -284,7 +354,7 @@ class FileController extends AbstractController
//
$id = intval(Request::input('id'));
//
$row = File::permissionFind($id);
$row = File::permissionFind($id, $user);
//
$userid = $user->userid;
if ($row->pid > 0) {
@ -306,10 +376,11 @@ class FileController extends AbstractController
'userid' => $userid,
'created_id' => $user->userid,
]);
$file->handleDuplicateName();
$data = AbstractModel::transaction(function() use ($file) {
$content = FileContent::select(['content', 'text', 'size'])->whereFid($file->cid)->orderByDesc('id')->first();
$file->size = $content?->size ?: 0;
$file->saveBeforePids();
$file->saveBeforePP();
if ($content) {
$content = $content->toArray();
$content['fid'] = $file->id;
@ -340,7 +411,7 @@ class FileController extends AbstractController
*/
public function move()
{
User::auth();
$user = User::auth();
//
$ids = Request::input('ids');
$pid = intval(Request::input('pid'));
@ -351,29 +422,44 @@ class FileController extends AbstractController
if (count($ids) > 100) {
return Base::retError('一次最多只能移动100个文件或文件夹');
}
$toShareFile = false;
if ($pid > 0) {
File::permissionFind($pid, 1);
$tmpFile = File::permissionFind($pid, $user, 1);
$toShareFile = $tmpFile->getShareInfo();
}
//
$files = [];
AbstractModel::transaction(function() use ($pid, $ids, &$files) {
AbstractModel::transaction(function() use ($user, $pid, $ids, $toShareFile, &$files) {
foreach ($ids as $id) {
$file = File::permissionFind($id, 1000);
$file = File::permissionFind($id, $user, 1000);
//
if ($pid > 0) {
$arr = [];
$tid = $pid;
while ($tid > 0) {
$arr[] = $tid;
$tid = intval(File::whereId($tid)->value('pid'));
if ($toShareFile) {
if ($file->share) {
throw new ApiException("{$file->name} 当前正在共享,无法移动到另一个共享文件夹内");
}
if ($file->isSubShare()) {
throw new ApiException("{$file->name} 内含有共享文件,无法移动到另一个共享文件夹内");
}
$file->userid = $toShareFile->userid;
File::where('pids', 'LIKE', "%,{$file->id},%")->update(['userid' => $toShareFile->userid]);
}
if (in_array($id, $arr)) {
throw new ApiException('移动位置错误');
//
$tmpId = $pid;
while ($tmpId > 0) {
if ($id == $tmpId) {
throw new ApiException('移动位置错误');
}
$tmpId = intval(File::whereId($tmpId)->value('pid'));
}
} else {
$file->userid = $user->userid;
File::where('pids', 'LIKE', "%,{$file->id},%")->update(['userid' => $user->userid]);
}
//
$file->pid = $pid;
$file->saveBeforePids();
$file->handleDuplicateName();
$file->saveBeforePP();
$files[] = $file;
}
});
@ -399,7 +485,7 @@ class FileController extends AbstractController
*/
public function remove()
{
User::auth();
$user = User::auth();
//
$ids = Request::input('ids');
//
@ -411,9 +497,9 @@ class FileController extends AbstractController
}
//
$files = [];
AbstractModel::transaction(function() use ($ids, &$files) {
AbstractModel::transaction(function() use ($user, $ids, &$files) {
foreach ($ids as $id) {
$file = File::permissionFind($id, 1000);
$file = File::permissionFind($id, $user, 1000);
$file->deleteFile();
$files[] = $file;
}
@ -438,7 +524,9 @@ class FileController extends AbstractController
* - yes
* @apiParam {String} down 直接下载
* - no: 浏览(默认)
* - yes: 下载office文件直接下载
* - yes: 下载office文件直接下载除非是preview
* - preview: 转预览地址
* @apiParam {Number} [history_id] 读取历史记录ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -449,15 +537,21 @@ class FileController extends AbstractController
$id = Request::input('id');
$down = Request::input('down', 'no');
$only_update_at = Request::input('only_update_at', 'no');
$history_id = intval(Request::input('history_id'));
//
if (Base::isNumber($id)) {
User::auth();
$file = File::permissionFind(intval($id));
$user = User::auth();
$file = File::permissionFind(intval($id), $user);
} elseif ($id) {
$fileLink = FileLink::whereCode($id)->first();
$file = $fileLink?->file;
if (empty($file)) {
return Base::retError('链接不存在');
$msg = '文件链接不存在';
$data = File::code2IdName($id);
if ($data) {
$msg = "{$data->name}{$msg}";
}
return Base::retError($msg, $data);
}
} else {
return Base::retError('参数错误');
@ -470,7 +564,14 @@ class FileController extends AbstractController
]);
}
//
$content = FileContent::whereFid($file->id)->orderByDesc('id')->first();
$builder = FileContent::whereFid($file->id);
if ($history_id > 0) {
$builder->whereId($history_id);
}
$content = $builder->orderByDesc('id')->first();
if ($down === 'preview') {
return Redirect::to(FileContent::formatPreview($file, $content?->content));
}
return FileContent::formatContent($file, $content?->content, $down == 'yes');
}
@ -492,25 +593,25 @@ class FileController extends AbstractController
*/
public function content__save()
{
Base::checkClientVersion('0.9.13');
$user = User::auth();
//
$id = Base::getPostInt('id');
$content = Base::getPostValue('content');
//
$file = File::permissionFind($id, 1);
$file = File::permissionFind($id, $user, 1);
//
$text = '';
if ($file->type == 'document') {
$data = Base::json2array($content);
$isRep = false;
preg_match_all("/<img\s*src=\"data:image\/(png|jpg|jpeg);base64,(.*?)\"/s", $data['content'], $matchs);
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg);base64,(.*?)\"/s", $data['content'], $matchs);
foreach ($matchs[2] as $key => $text) {
$tmpPath = "uploads/file/document/" . date("Ym") . "/" . $id . "/attached/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5($text) . "." . $matchs[1][$key];
if (file_put_contents(public_path($tmpPath), base64_decode($text))) {
$data['content'] = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($tmpPath) . '"', $data['content']);
$paramet = getimagesize(public_path($tmpPath));
$data['content'] = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($tmpPath) . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $data['content']);
$isRep = true;
}
}
@ -535,9 +636,10 @@ class FileController extends AbstractController
$contentString = $content;
$file->ext = 'mind';
break;
case 'code':
case 'txt':
$contentString = $content;
case 'code':
$contentArray = Base::json2array($content);
$contentString = $contentArray['content'];
break;
default:
return Base::retError('参数错误');
@ -589,7 +691,7 @@ class FileController extends AbstractController
$key = Request::input('key');
$url = Request::input('url');
//
$file = File::permissionFind($id, 1);
$file = File::permissionFind($id, $user, 1);
//
if ($status === 2) {
$parse = parse_url($url);
@ -628,7 +730,7 @@ class FileController extends AbstractController
* @apiGroup file
* @apiName content__upload
*
* @apiParam {Number} [pid] 父级ID
* @apiParam {Number} [pid] 父级ID
* @apiParam {String} [files] 文件名
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@ -647,7 +749,7 @@ class FileController extends AbstractController
if (File::wherePid($pid)->count() >= 300) {
return Base::retError('每个文件夹里最多只能创建300个文件或文件夹');
}
$row = File::permissionFind($pid, 1);
$row = File::permissionFind($pid, $user, 1);
$userid = $row->userid;
} else {
if (File::whereUserid($user->userid)->wherePid(0)->count() >= 300) {
@ -656,11 +758,11 @@ class FileController extends AbstractController
}
//
$dirs = explode("/", $webkitRelativePath);
$addItem = [];
while (count($dirs) > 1) {
$dirName = array_shift($dirs);
if ($dirName) {
$pushMsg = [];
AbstractModel::transaction(function () use ($dirName, $user, $userid, &$pid, &$pushMsg) {
AbstractModel::transaction(function () use ($dirName, $user, $userid, &$pid, &$addItem) {
$dirRow = File::wherePid($pid)->whereType('folder')->whereName($dirName)->lockForUpdate()->first();
if (empty($dirRow)) {
$dirRow = File::createInstance([
@ -670,8 +772,9 @@ class FileController extends AbstractController
'userid' => $userid,
'created_id' => $user->userid,
]);
if ($dirRow->saveBeforePids()) {
$pushMsg[] = File::find($dirRow->id);
$dirRow->handleDuplicateName();
if ($dirRow->saveBeforePP()) {
$addItem[] = File::find($dirRow->id);
}
}
if (empty($dirRow)) {
@ -679,7 +782,7 @@ class FileController extends AbstractController
}
$pid = $dirRow->id;
});
foreach ($pushMsg as $tmpRow) {
foreach ($addItem as $tmpRow) {
$tmpRow->pushMsg('add', $tmpRow);
}
}
@ -718,7 +821,7 @@ class FileController extends AbstractController
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx' => "code",
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx', 'plist' => "code",
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm' => "media",
'xmind' => "xmind",
@ -736,20 +839,26 @@ class FileController extends AbstractController
'userid' => $userid,
'created_id' => $user->userid,
]);
$file->handleDuplicateName();
// 开始创建
return AbstractModel::transaction(function () use ($webkitRelativePath, $type, $user, $data, $file) {
return AbstractModel::transaction(function () use ($addItem, $webkitRelativePath, $type, $user, $data, $file) {
$file->size = $data['size'] * 1024;
$file->saveBeforePids();
$file->saveBeforePP();
//
$data = Base::uploadMove($data, "uploads/file/" . $file->type . "/" . date("Ym") . "/" . $file->id . "/");
$content = [
'from' => '',
'type' => $type,
'ext' => $data['ext'],
'url' => $data['path'],
];
if (isset($data['width'])) {
$content['width'] = $data['width'];
$content['height'] = $data['height'];
}
$content = FileContent::createInstance([
'fid' => $file->id,
'content' => [
'from' => '',
'type' => $type,
'ext' => $data['ext'],
'url' => $data['path']
],
'content' => $content,
'text' => '',
'size' => $file->size,
'userid' => $user->userid,
@ -759,15 +868,88 @@ class FileController extends AbstractController
$tmpRow = File::find($file->id);
$tmpRow->pushMsg('add', $tmpRow);
//
$data = $tmpRow->toArray();
$data = File::handleImageUrl($tmpRow->toArray());
$data['full_name'] = $webkitRelativePath ?: $data['name'];
File::handleImageUrl($data);
return Base::retSuccess($data['name'] . ' 上传成功', $data);
//
$addItem[] = $data;
return Base::retSuccess($data['name'] . ' 上传成功', $addItem);
});
}
/**
* @api {get} api/file/share 12. 获取共享信息
* @api {get} api/file/content/history 12. 获取内容历史
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup file
* @apiName content__history
*
* @apiParam {Number} id 文件ID
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:100
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function content__history()
{
$user = User::auth();
//
$id = Request::input('id');
//
$file = File::permissionFind(intval($id), $user);
//
$data = FileContent::select(['id', 'size', 'userid', 'created_at'])
->whereFid($file->id)
->orderByDesc('id')
->paginate(Base::getPaginate(100, 20));
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/file/content/restore 13. 恢复文件历史
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup file
* @apiName content__restore
*
* @apiParam {Number} id 文件ID
* @apiParam {Number} history_id 历史数据ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function content__restore()
{
$user = User::auth();
//
$id = intval(Request::input('id'));
$history_id = intval(Request::input('history_id'));
//
$file = File::permissionFind($id, $user);
//
$history = FileContent::whereFid($file->id)->whereId($history_id)->first();
if (empty($history)) {
return Base::retError('历史数据不存在或已被删除');
}
//
$content = $history->replicate();
$content->userid = $user->userid;
$content->save();
//
$file->size = $content->size;
$file->save();
$file->pushMsg('content');
//
return Base::retSuccess('还原成功');
}
/**
* @api {get} api/file/share 14. 获取共享信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -803,7 +985,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/share/update 13. 设置共享
* @api {get} api/file/share/update 15. 设置共享
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -845,8 +1027,10 @@ class FileController extends AbstractController
return Base::retError('仅限所有者操作');
}
//
if ($file->isNnShare()) {
return Base::retError('已经处于共享文件夹中');
$share = $file->isNnShare();
if ($share) {
$typeCn = $file->type === 'folder' ? '文件夹' : '文件';
return Base::retError("{$typeCn}已经处于【{$share->name}】共享文件夹中,无法重复共享");
}
//
if (!is_array($userids) || empty($userids)) {
@ -866,7 +1050,7 @@ class FileController extends AbstractController
// 设置共享
$action = "update";
if ($force === 0) {
if (File::where("pids", "like", "%,{$file->id},%")->whereShare(1)->exists()) {
if ($file->isSubShare()) {
return Base::retError('此文件夹内已有共享文件夹', [], -3001);
}
}
@ -891,7 +1075,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/share/out 14. 退出共享
* @api {get} api/file/share/out 16. 退出共享
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -910,7 +1094,7 @@ class FileController extends AbstractController
//
$id = intval(Request::input('id'));
//
$file = File::permissionFind($id);
$file = File::permissionFind($id, $user);
//
if ($file->userid == $user->userid) {
return Base::retError('不能退出自己共享的文件');
@ -925,7 +1109,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/link 15. 获取链接
* @api {get} api/file/link 17. 获取链接
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -948,29 +1132,9 @@ class FileController extends AbstractController
$id = intval(Request::input('id'));
$refresh = Request::input('refresh', 'no');
//
$file = File::permissionFind($id);
if ($file->type == 'folder') {
return Base::retError('文件夹暂不支持此功能');
}
$file = File::permissionFind($id, $user);
$fileLink = $file->getShareLink($user->userid, $refresh == 'yes');
//
$fileLink = FileLink::whereFileId($file->id)->whereUserid($user->userid)->first();
if (empty($fileLink)) {
$fileLink = FileLink::createInstance([
'file_id' => $file->id,
'userid' => $user->userid,
'code' => Base::generatePassword(64),
]);
$fileLink->save();
} else {
if ($refresh == 'yes') {
$fileLink->code = Base::generatePassword(64);
$fileLink->save();
}
}
return Base::retSuccess('success', [
'id' => $file->id,
'url' => Base::fillUrl('single/file/' . $fileLink->code),
'num' => $fileLink->num
]);
return Base::retSuccess('success', $fileLink);
}
}

View File

@ -4,7 +4,9 @@ namespace App\Http\Controllers\Api;
use App\Exceptions\ApiException;
use App\Models\AbstractModel;
use App\Models\Deleted;
use App\Models\File;
use App\Models\FileContent;
use App\Models\Project;
use App\Models\ProjectColumn;
use App\Models\ProjectFlow;
@ -19,9 +21,13 @@ use App\Models\User;
use App\Models\WebSocketDialog;
use App\Module\Base;
use App\Module\BillExport;
use App\Module\BillMultipleExport;
use App\Module\Doo;
use App\Module\TimeRange;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Madzipper;
use Redirect;
use Request;
use Response;
use Session;
@ -42,6 +48,10 @@ class ProjectController extends AbstractController
* @apiName lists
*
* @apiParam {String} [all] 是否查看所有项目(限制管理员)
* @apiParam {String} [type] 项目类型
* - all全部默认
* - team团队项目
* - personal个人项目
* @apiParam {String} [archived] 归档状态
* - all全部
* - no未归档默认
@ -51,6 +61,9 @@ class ProjectController extends AbstractController
* - yes取列表
* @apiParam {Object} [keys] 搜索条件
* - keys.name: 项目名称
* @apiParam {String} [timerange] 时间范围1678248944,1678248944
* - 第一个时间: 读取在这个时间之后更新的数据
* - 第二个时间: 读取在这个时间之后删除的数据ID第1页附加返回数据: deleted_id
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:50,最大:100
@ -94,8 +107,11 @@ class ProjectController extends AbstractController
$user = User::auth();
//
$all = Request::input('all');
$type = Request::input('type', 'all');
$archived = Request::input('archived', 'no');
$getcolumn = Request::input('getcolumn', 'no');
$keys = Request::input('keys');
$timerange = TimeRange::parse(Request::input('timerange'));
//
if ($all) {
$user->identity('admin');
@ -108,30 +124,41 @@ class ProjectController extends AbstractController
$builder->with(['projectColumn']);
}
//
if ($type === 'team') {
$builder->where('projects.personal', 0);
} elseif ($type === 'personal') {
$builder->where('projects.personal', 1);
}
//
if ($archived == 'yes') {
$builder->whereNotNull('projects.archived_at');
} elseif ($archived == 'no') {
$builder->whereNull('projects.archived_at');
}
//
$keys = Request::input('keys');
if (is_array($keys) || $timerange->updated) {
$totalAll = $builder->clone()->count();
}
//
if (is_array($keys)) {
$buildClone = $builder->clone();
if ($keys['name']) {
$builder->where("projects.name", "like", "%{$keys['name']}%");
}
}
//
if ($timerange->updated) {
$builder->where('projects.updated_at', '>', $timerange->updated);
}
//
$list = $builder->orderByDesc('projects.id')->paginate(Base::getPaginate(100, 50));
$list->transform(function (Project $project) use ($user) {
return array_merge($project->toArray(), $project->getTaskStatistics($user->userid));
});
//
$data = $list->toArray();
if (isset($buildClone)) {
$data['total_all'] = $buildClone->count();
} else {
$data['total_all'] = $data['total'];
$data['total_all'] = $totalAll ?? $data['total'];
if ($list->currentPage() === 1) {
$data['deleted_id'] = Deleted::ids('project', $user->userid, $timerange->deleted);
}
//
return Base::retSuccess('success', $data);
@ -212,6 +239,7 @@ class ProjectController extends AbstractController
* @apiParam {String} [flow] 开启流程
* - open: 开启
* - close: 关闭(默认)
* @apiParam {Number} [personal] 个人项目,注册成功时创建(仅支持创建一个个人项目)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -220,73 +248,8 @@ class ProjectController extends AbstractController
public function add()
{
$user = User::auth();
// 项目名称
$name = trim(Request::input('name', ''));
$desc = trim(Request::input('desc', ''));
$flow = trim(Request::input('flow', 'close'));
if (mb_strlen($name) < 2) {
return Base::retError('项目名称不可以少于2个字');
} elseif (mb_strlen($name) > 32) {
return Base::retError('项目名称最多只能设置32个字');
}
if (mb_strlen($desc) > 255) {
return Base::retError('项目介绍最多只能设置255个字');
}
// 列表
$columns = explode(",", Request::input('columns'));
$insertColumns = [];
$sort = 0;
foreach ($columns AS $column) {
$column = trim($column);
if ($column) {
$insertColumns[] = [
'name' => $column,
'sort' => $sort++,
];
}
}
if (empty($insertColumns)) {
$insertColumns[] = [
'name' => 'Default',
'sort' => 0,
];
}
if (count($insertColumns) > 30) {
return Base::retError('项目列表最多不能超过30个');
}
// 开始创建
$project = Project::createInstance([
'name' => $name,
'desc' => $desc,
'userid' => $user->userid,
]);
AbstractModel::transaction(function() use ($flow, $insertColumns, $project) {
$project->save();
ProjectUser::createInstance([
'project_id' => $project->id,
'userid' => $project->userid,
'owner' => 1,
])->save();
foreach ($insertColumns AS $column) {
$column['project_id'] = $project->id;
ProjectColumn::createInstance($column)->save();
}
$dialog = WebSocketDialog::createGroup(null, $project->userid, 'project');
if (empty($dialog)) {
throw new ApiException('创建项目聊天室失败');
}
$project->dialog_id = $dialog->id;
$project->save();
//
if ($flow == 'open') {
$project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-14,"name":"已取消","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0}]'));
}
});
//
$data = Project::find($project->id);
$data->addLog("创建项目");
$data->pushMsg('add', $data);
return Base::retSuccess('添加成功', $data);
return Project::createProject(Request::all(), $user->userid);
}
/**
@ -322,18 +285,22 @@ class ProjectController extends AbstractController
}
//
$project = Project::userProject($project_id, true, true);
//
if ($project->name != $name) {
$project->addLog("修改项目名称", [
'change' => [$project->name, $name]
]);
$project->name = $name;
}
if ($project->desc != $desc) {
$project->desc = $desc;
$project->addLog("修改项目介绍");
}
$project->save();
AbstractModel::transaction(function () use ($desc, $name, $project) {
if ($project->name != $name) {
$project->addLog("修改项目名称", [
'change' => [$project->name, $name]
]);
$project->name = $name;
if ($project->dialog_id) {
WebSocketDialog::updateData(['id' => $project->dialog_id], ['name' => $project->name]);
}
}
if ($project->desc != $desc) {
$project->desc = $desc;
$project->addLog("修改项目介绍");
}
$project->save();
});
$project->pushMsg('update', $project);
//
return Base::retSuccess('修改成功', $project);
@ -382,6 +349,8 @@ class ProjectController extends AbstractController
}
$project->syncDialogUser();
$project->addLog("修改项目成员");
$project->user_simple = count($array) . "|" . implode(",", array_slice($array, 0, 3));
$project->save();
return $deleteUser->toArray();
});
//
@ -543,11 +512,11 @@ class ProjectController extends AbstractController
$project = Project::userProject($project_id, true, true);
//
if (!User::whereUserid($owner_userid)->exists()) {
return Base::retError('员不存在');
return Base::retError('员不存在');
}
//
AbstractModel::transaction(function() use ($owner_userid, $project) {
ProjectUser::whereProjectId($project->id)->update(['owner' => 0]);
ProjectUser::whereProjectId($project->id)->change(['owner' => 0]);
ProjectUser::updateInsert([
'project_id' => $project->id,
'userid' => $owner_userid,
@ -608,11 +577,11 @@ class ProjectController extends AbstractController
if (!is_array($item['task'])) continue;
$index = 0;
foreach ($item['task'] as $task_id) {
if (ProjectTask::whereId($task_id)->whereProjectId($project->id)->whereCompleteAt(null)->update([
if (ProjectTask::whereId($task_id)->whereProjectId($project->id)->whereCompleteAt(null)->change([
'column_id' => $item['id'],
'sort' => $index
])) {
ProjectTask::whereParentId($task_id)->whereProjectId($project->id)->update([
ProjectTask::whereParentId($task_id)->whereProjectId($project->id)->change([
'column_id' => $item['id'],
]);
}
@ -878,14 +847,18 @@ class ProjectController extends AbstractController
* @apiName task__lists
*
* @apiParam {Object} [keys] 搜索条件
* - keys.name: 任务名称
* - keys.name: ID、任务名称
*
* @apiParam {Number} [project_id] 项目ID
* @apiParam {Number} [parent_id] 主任务IDproject_id && parent_id 0 仅查询自己参与的任务)
* - 大于0指定主任务下的子任务
* - 等于-1:表示仅主任务
* @apiParam {String} [name] 任务描述关键词
*
* @apiParam {Array} [time] 指定时间范围,如:['2020-12-12', '2020-12-30']
* @apiParam {String} [time_before] 指定时间之前2020-12-30 00:00:00(填写此项时 time 参数无效)
* @apiParam {String} [timerange] 时间范围1678248944,1678248944
* - 第一个时间: 读取在这个时间之后更新的数据
* - 第二个时间: 读取在这个时间之后删除的数据ID第1页附加返回数据: deleted_id
*
* @apiParam {String} [complete] 完成状态
* - all所有默认
* - yes已完成
@ -898,7 +871,8 @@ class ProjectController extends AbstractController
* - all所有
* - yes已删除
* - no未删除默认
* @apiParam {Object} sorts 排序方式
*
* @apiParam {Object} [sorts] 排序方式
* - sorts.complete_at 完成时间asc|desc
* - sorts.archived_at 归档时间asc|desc
* - sorts.end_at 到期时间asc|desc
@ -909,7 +883,7 @@ class ProjectController extends AbstractController
*/
public function task__lists()
{
User::auth();
$user = User::auth();
//
$builder = ProjectTask::with(['taskUser', 'taskTag']);
//
@ -917,7 +891,7 @@ class ProjectController extends AbstractController
$project_id = intval(Request::input('project_id'));
$name = Request::input('name');
$time = Request::input('time');
$time_before = Request::input('time_before');
$timerange = TimeRange::parse(Request::input('timerange'));
$complete = Request::input('complete', 'all');
$archived = Request::input('archived', 'no');
$deleted = Request::input('deleted', 'no');
@ -927,7 +901,11 @@ class ProjectController extends AbstractController
$sorts = is_array($sorts) ? $sorts : [];
//
if ($keys['name']) {
$builder->where("project_tasks.name", "like", "%{$keys['name']}%");
if (Base::isNumber($keys['name'])) {
$builder->where("project_tasks.id", intval($keys['name']));
} else {
$builder->where("project_tasks.name", "like", "%{$keys['name']}%");
}
}
//
$scopeAll = false;
@ -957,13 +935,14 @@ class ProjectController extends AbstractController
});
}
//
if (Base::isDateOrTime($time_before)) {
$builder->whereNotNull('project_tasks.end_at')->where('project_tasks.end_at', '<', Carbon::parse($time_before));
} elseif (is_array($time)) {
if (is_array($time)) {
if (Base::isDateOrTime($time[0]) && Base::isDateOrTime($time[1])) {
$builder->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay());
}
}
if ($timerange->updated) {
$builder->where('project_tasks.updated_at', '>', $timerange->updated);
}
//
if ($complete === 'yes') {
$builder->whereNotNull('project_tasks.complete_at');
@ -991,7 +970,12 @@ class ProjectController extends AbstractController
//
$list = $builder->orderByDesc('project_tasks.id')->paginate(Base::getPaginate(200, 100));
//
return Base::retSuccess('success', $list);
$data = $list->toArray();
if ($list->currentPage() === 1) {
$data['deleted_id'] = Deleted::ids('projectTask', $user->userid, $timerange->deleted);
}
//
return Base::retSuccess('success', $data);
}
/**
@ -1018,12 +1002,12 @@ class ProjectController extends AbstractController
//
$userid = Base::arrayRetainInt(Request::input('userid'), true);
$time = Request::input('time');
$type = Request::input('type','taskTime');
$type = Request::input('type', 'taskTime');
if (empty($userid) || empty($time)) {
return Base::retError('参数错误');
}
if (count($userid) > 20) {
return Base::retError('导出会员限制最多20个');
if (count($userid) > 100) {
return Base::retError('导出成员限制最多100个');
}
if (!(is_array($time) && Base::isDateOrTime($time[0]) && Base::isDateOrTime($time[1]))) {
return Base::retError('时间选择错误');
@ -1047,6 +1031,7 @@ class ProjectController extends AbstractController
$headings[] = '验收/测试用时';
$headings[] = '负责人';
$headings[] = '创建人';
$headings[] = '状态';
$datas = [];
//
$builder = ProjectTask::select(['project_tasks.*', 'project_task_users.userid as ownerid'])
@ -1054,22 +1039,18 @@ class ProjectController extends AbstractController
->where('project_task_users.owner', 1)
->whereIn('project_task_users.userid', $userid)
->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay(), $type);
$builder->orderByDesc('project_tasks.id')->chunk(100, function($tasks) use (&$datas) {
$builder->orderByDesc('project_tasks.id')->chunk(100, function ($tasks) use (&$datas) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$flowChanges = ProjectTaskFlowChange::whereTaskId($task->id)->get();
$developTime = 0;//开发时间
$testTime = 0;//验收/测试时间
$taskStartTime = $task->start_at ? Carbon::parse($task->start_at)->timestamp : Carbon::parse($task->created_at)->timestamp;
$taskCompleteTime = $task->complete_at ? Carbon::parse($task->complete_at)->timestamp : time();
$totalTime = $taskCompleteTime - $taskStartTime; //开发测试总用时
foreach ($flowChanges as $change) {
if (!str_contains($change->before_flow_item_name, 'end')) {
$upOne = ProjectTaskFlowChange::where('id', '<', $change->id)->whereTaskId($task->id)->orderByDesc('id')->first();
if ($upOne) {
if (str_contains($change->before_flow_item_name, 'progress') && str_contains($change->before_flow_item_name, '进行')) {
$devCtime = Carbon::parse($change->created_at)->timestamp;
$oCtime = Carbon::parse($upOne->created_at)->timestamp;
$minusNum = $devCtime - $oCtime;
$developTime += $minusNum;
}
if (str_contains($change->before_flow_item_name, 'test') || str_contains($change->before_flow_item_name, '测试') || strpos($change->before_flow_item_name, '验收') !== false) {
$testCtime = Carbon::parse($change->created_at)->timestamp;
$tTime = Carbon::parse($upOne->created_at)->timestamp;
@ -1083,29 +1064,11 @@ class ProjectController extends AbstractController
$lastChange = ProjectTaskFlowChange::whereTaskId($task->id)->orderByDesc('id')->first();
$nowTime = time();
$unFinishTime = $nowTime - Carbon::parse($lastChange->created_at)->timestamp;
if (str_contains($lastChange->after_flow_item_name, 'progress') || str_contains($lastChange->after_flow_item_name, '进行')) {
$developTime += $unFinishTime;
} elseif (str_contains($lastChange->after_flow_item_name, 'test') || str_contains($lastChange->after_flow_item_name, '测试') || strpos($lastChange->after_flow_item_name, '验收') !== false) {
if (str_contains($lastChange->after_flow_item_name, 'test') || str_contains($lastChange->after_flow_item_name, '测试') || strpos($lastChange->after_flow_item_name, '验收') !== false) {
$testTime += $unFinishTime;
}
}
$firstChange = ProjectTaskFlowChange::whereTaskId($task->id)->orderBy('id')->first();
if (str_contains($firstChange->after_flow_item_name, 'end')) {
$firstDevTime = Carbon::parse($firstChange->created_at)->timestamp - Carbon::parse($task->created_at)->timestamp;
$developTime += $firstDevTime;
}
if (count($flowChanges) === 0 && $task->start_at) {
$lastTime = $task->complete_at ? Carbon::parse($task->complete_at)->timestamp : time();
$developTime = $lastTime - Carbon::parse($task->start_at)->timestamp;
}
$totalTime = $developTime + $testTime; //任务总用时
if ($task->complete_at) {
$a = Carbon::parse($task->complete_at)->timestamp;
if ($task->start_at) {
$b = Carbon::parse($task->start_at)->timestamp;
$totalTime = $a - $b;
}
}
$developTime = $totalTime - $testTime;//开发时间
$planTime = '-';//任务计划用时
$overTime = '-';//超时时间
if ($task->end_at) {
@ -1119,7 +1082,35 @@ class ProjectController extends AbstractController
$planTime = Base::timeDiff($startTime, $endTime);
}
$actualTime = $task->complete_at ? $totalTime : 0;//实际完成用时
$datas[] = [
$statusText = '未完成';
if ($task->flow_item_name) {
if (str_contains($task->flow_item_name, '已取消')) {
$statusText = '已取消';
$actualTime = 0;
$testTime = 0;
$developTime = 0;
$overTime = '-';
} elseif (str_contains($task->flow_item_name, '已完成')) {
$statusText = '已完成';
}
} elseif ($task->complete_at) {
$statusText = '已完成';
}
if (!isset($datas[$task->ownerid])) {
$datas[$task->ownerid] = [
'index' => 1,
'nickname' => Base::filterEmoji(User::userid2nickname($task->ownerid)),
'styles' => ["A1:P1" => ["font" => ["bold" => true]]],
'data' => [],
];
}
$datas[$task->ownerid]['index']++;
if ($statusText === '未完成') {
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "ff0000"]]]; // 未完成
} elseif ($statusText === '已完成' && $task->end_at && Carbon::parse($task->complete_at)->gt($task->end_at)) {
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "436FF6"]]]; // 已完成超期
}
$datas[$task->ownerid]['data'][] = [
$task->id,
$task->parent_id ?: '-',
Base::filterEmoji($task->project?->name) ?: '-',
@ -1131,13 +1122,28 @@ class ProjectController extends AbstractController
$planTime ?: '-',
$actualTime ? Base::timeFormat($actualTime) : '-',
$overTime,
$developTime > 0? Base::timeFormat($developTime) : '-',
$developTime > 0 ? Base::timeFormat($developTime) : '-',
$testTime > 0 ? Base::timeFormat($testTime) : '-',
Base::filterEmoji(User::userid2nickname($task->ownerid)) . " (ID: {$task->ownerid})",
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
$statusText
];
}
});
if (empty($datas)) {
return Base::retError('没有任何数据');
}
//
$sheets = [];
foreach ($userid as $ownerid) {
$data = $datas[$ownerid] ?? [
'nickname' => Base::filterEmoji(User::userid2nickname($ownerid)),
'styles' => ["A1:P1" => ["font" => ["bold" => true]]],
'data' => [],
];
$title = (count($sheets) + 1) . "." . ($data['nickname'] ?: $ownerid);
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($data['data'])->setStyles($data['styles']);
}
//
$fileName = User::userid2nickname($userid[0]) ?: $userid[0];
if (count($userid) > 1) {
@ -1145,19 +1151,21 @@ class ProjectController extends AbstractController
}
$fileName .= '任务统计_' . Base::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Base::time());
$res = BillExport::create()->setHeadings($headings)->setData($datas)->store($filePath . "/" . $fileName);
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
return Base::retError('导出失败,' . $fileName . '');
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls'). ".zip";
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Exception) { }
} catch (\Throwable) {
}
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
@ -1174,11 +1182,119 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/down 20. 导出任务(限管理员)
* @api {get} api/project/task/exportoverdue 20. 导出超期任务(限管理员)
*
* @apiDescription 导出指定范围任务已完成、未完成、已归档返回下载地址需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__exportoverdue
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function task__exportoverdue()
{
$user = User::auth('admin');
//
$headings = [];
$headings[] = '任务ID';
$headings[] = '父级任务ID';
$headings[] = '所属项目';
$headings[] = '任务标题';
$headings[] = '任务开始时间';
$headings[] = '任务结束时间';
$headings[] = '任务计划用时';
$headings[] = '超时时间';
$headings[] = '负责人';
$headings[] = '创建人';
$data = [];
//
ProjectTask::whereNull('complete_at')
->whereNotNull('end_at')
->where('end_at', '<=', Carbon::now())
->orderBy('end_at')
->chunk(100, function ($tasks) use (&$data) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$taskStartTime = Carbon::parse($task->start_at ?: $task->created_at)->timestamp;
$totalTime = time() - $taskStartTime; //开发测试总用时
$planTime = '-';//任务计划用时
$overTime = '-';//超时时间
if ($task->end_at) {
$startTime = Carbon::parse($task->start_at)->timestamp;
$endTime = Carbon::parse($task->end_at)->timestamp;
$planTotalTime = $endTime - $startTime;
$residueTime = $planTotalTime - $totalTime;
if ($residueTime < 0) {
$overTime = Base::timeFormat(abs($residueTime));
}
$planTime = Base::timeDiff($startTime, $endTime);
}
$ownerIds = $task->taskUser->where('owner', 1)->pluck('userid')->toArray();
$ownerNames = [];
foreach ($ownerIds as $ownerId) {
$ownerNames[] = Base::filterEmoji(User::userid2nickname($ownerId)) . " (ID: {$ownerId})";
}
$data[] = [
$task->id,
$task->parent_id ?: '-',
Base::filterEmoji($task->project?->name) ?: '-',
Base::filterEmoji($task->name),
$task->start_at ?: '-',
$task->end_at ?: '-',
$planTime ?: '-',
$overTime,
implode("", $ownerNames),
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
];
}
});
if (empty($data)) {
return Base::retError('没有任何数据');
}
//
$sheets = [
BillExport::create()->setTitle("超期任务")->setHeadings($headings)->setData($data)->setStyles(["A1:J1" => ["font" => ["bold" => true]]])
];
//
$fileName = '超期任务_' . Base::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Base::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
return Base::retError('导出失败,' . $fileName . '');
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Throwable) {
}
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
Session::put('task::export:userid', $user->userid);
return Base::retSuccess('success', [
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
'url' => Base::fillUrl('api/project/task/down?key=' . urlencode($base64)),
]);
} else {
return Base::retError('打包失败,请稍后再试...');
}
}
/**
* @api {get} api/project/task/down 21. 下载导出的任务
*
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__down
*
* @apiParam {String} key 通过export接口得到的下载钥匙
@ -1197,11 +1313,11 @@ class ProjectController extends AbstractController
if (empty($file) || !file_exists(storage_path($file))) {
return Base::ajaxError("文件不存在!", [], 0, 502);
}
return response()->download(storage_path($file));
return Response::download(storage_path($file));
}
/**
* @api {get} api/project/task/one 21. 获取单个任务信息
* @api {get} api/project/task/one 22. 获取单个任务信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -1235,7 +1351,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/content 22. 获取任务详细描述
* @api {get} api/project/task/content 23. 获取任务详细描述
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -1263,7 +1379,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/files 23. 获取任务文件列表
* @api {get} api/project/task/files 24. 获取任务文件列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -1288,7 +1404,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/filedelete 24. 删除任务文件
* @api {get} api/project/task/filedelete 25. 删除任务文件
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@ -1321,7 +1437,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/filedetail 25. 获取任务文件详情
* @api {get} api/project/task/filedetail 26. 获取任务文件详情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -1365,7 +1481,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/filedown 26. 下载任务文件
* @api {get} api/project/task/filedown 27. 下载任务文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -1373,6 +1489,9 @@ class ProjectController extends AbstractController
* @apiName task__filedown
*
* @apiParam {Number} file_id 文件ID
* @apiParam {String} down 直接下载
* - yes: 下载(默认)
* - preview: 转预览地址
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -1383,6 +1502,7 @@ class ProjectController extends AbstractController
User::auth();
//
$file_id = intval(Request::input('file_id'));
$down = Request::input('down', 'yes');
//
$file = ProjectTaskFile::find($file_id);
if (empty($file)) {
@ -1391,15 +1511,26 @@ class ProjectController extends AbstractController
//
try {
ProjectTask::userTask($file->task_id, null);
} catch (\Exception $e) {
} catch (\Throwable $e) {
abort(403, $e->getMessage() ?: "This file not support download.");
}
//
return Response::download(public_path($file->getRawOriginal('path')), $file->name);
if ($down === 'preview') {
return Redirect::to(FileContent::toPreviewUrl([
'ext' => $file->ext,
'name' => $file->name,
'path' => $file->getRawOriginal('path'),
]));
}
//
$filePath = public_path($file->getRawOriginal('path'));
return Base::streamDownload(function() use ($filePath) {
echo file_get_contents($filePath);
}, $file->name);
}
/**
* @api {post} api/project/task/add 27. 添加任务
* @api {post} api/project/task/add 28. 添加任务
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -1466,11 +1597,12 @@ class ProjectController extends AbstractController
$data['new_column'] = $newColumn;
}
$task->pushMsg('add', $data);
$task->taskPush(null, 0);
return Base::retSuccess('添加成功', $data);
}
/**
* @api {get} api/project/task/addsub 28. 添加子任务
* @api {get} api/project/task/addsub 29. 添加子任务
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@ -1510,7 +1642,7 @@ class ProjectController extends AbstractController
}
/**
* @api {post} api/project/task/update 29. 修改任务、子任务
* @api {post} api/project/task/update 30. 修改任务、子任务
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@ -1520,6 +1652,7 @@ class ProjectController extends AbstractController
* @apiParam {Number} task_id 任务ID
* @apiParam {String} [name] 任务描述
* @apiParam {Array} [times] 计划时间(格式:开始时间,结束时间2020-01-01 00:00,2020-01-01 23:59
* @apiParam {String} [loop] 重复周期,数字代表天数(子任务不支持)
* @apiParam {Array} [owner] 修改负责人
* @apiParam {String} [content] 任务详情(子任务不支持)
* @apiParam {String} [color] 背景色(子任务不支持)
@ -1556,7 +1689,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/dialog 30. 创建/获取聊天室
* @api {get} api/project/task/dialog 31. 创建/获取聊天室
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -1571,7 +1704,7 @@ class ProjectController extends AbstractController
*/
public function task__dialog()
{
User::auth();
$user = User::auth();
//
$task_id = intval(Request::input('task_id'));
//
@ -1584,7 +1717,7 @@ class ProjectController extends AbstractController
AbstractModel::transaction(function() use ($task) {
if (empty($task->dialog_id)) {
$task->lockForUpdate();
$dialog = WebSocketDialog::createGroup(null, $task->relationUserids(), 'task');
$dialog = WebSocketDialog::createGroup($task->name, $task->relationUserids(), 'task');
if ($dialog) {
$task->dialog_id = $dialog->id;
$task->save();
@ -1596,14 +1729,16 @@ class ProjectController extends AbstractController
});
//
$task->pushMsg('dialog');
$dialogData = WebSocketDialog::find($task->dialog_id)?->formatData($user->userid);
return Base::retSuccess('success', [
'id' => $task->id,
'dialog_id' => $task->dialog_id,
'dialog_data' => $dialogData,
]);
}
/**
* @api {get} api/project/task/archived 31. 归档任务
* @api {get} api/project/task/archived 32. 归档任务
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@ -1645,7 +1780,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/remove 32. 删除任务
* @api {get} api/project/task/remove 33. 删除任务
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@ -1670,7 +1805,7 @@ class ProjectController extends AbstractController
//
$task = ProjectTask::userTask($task_id, null, $type !== 'recovery', true);
if ($type == 'recovery') {
$task->recoveryTask();
$task->restoreTask();
return Base::retSuccess('操作成功', ['id' => $task->id]);
} else {
$task->deleteTask();
@ -1679,7 +1814,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/resetfromlog 33. 根据日志重置任务
* @api {get} api/project/task/resetfromlog 34. 根据日志重置任务
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@ -1738,7 +1873,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/flow 34. 任务工作流信息
* @api {get} api/project/task/flow 35. 任务工作流信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -1820,7 +1955,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/flow/list 35. 工作流列表
* @api {get} api/project/flow/list 36. 工作流列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -1846,7 +1981,7 @@ class ProjectController extends AbstractController
}
/**
* @api {post} api/project/flow/save 36. 保存工作流
* @api {post} api/project/flow/save 37. 保存工作流
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
@ -1880,7 +2015,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/flow/delete 37. 删除工作流
* @api {get} api/project/flow/delete 38. 删除工作流
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
@ -1912,7 +2047,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/log/lists 38. 获取项目、任务日志
* @api {get} api/project/log/lists 39. 获取项目、任务日志
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -1951,11 +2086,12 @@ class ProjectController extends AbstractController
if ($task_id === 0) {
$log->projectTask?->cancelAppend();
}
$log->detail = Doo::translate($log->detail);
$log->time = [
'ymd' => date(date("Y", $timestamp) == date("Y", Base::time()) ? "m-d" : "Y-m-d", $timestamp),
'hi' => date("h:i", $timestamp) ,
'week' => "" . Base::getTimeWeek($timestamp),
'segment' => Base::getTimeDayeSegment($timestamp),
'week' => Doo::translate("" . Base::getTimeWeek($timestamp)),
'segment' => Doo::translate(Base::getTimeDayeSegment($timestamp)),
];
return $log;
});
@ -1964,7 +2100,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/top 39. 项目置顶
* @api {get} api/project/top 40. 项目置顶
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0

View File

@ -0,0 +1,179 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\User;
use App\Models\UserCheckinMac;
use App\Models\UserCheckinRecord;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use Cache;
use Carbon\Carbon;
use Request;
/**
* @apiDefine public
*
* 公开
*/
class PublicController extends AbstractController
{
/**
* 签到 - 路由器openwrt功能安装脚本
*
* @apiParam {String} key
*
* @return string
*/
public function checkin__install()
{
$key = trim(Request::input('key'));
//
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return <<<EOF
#!/bin/sh
echo "function off"
EOF;
}
if ($key != $setting['key']) {
return <<<EOF
#!/bin/sh
echo "key error"
EOF;
}
//
$reportUrl = Base::fillUrl("api/public/checkin/report");
return <<<EOE
#!/bin/sh
echo 'installing...'
cat > /etc/init.d/dootask-checkin-report <<EOF
#!/bin/sh
mac=\\\$(awk 'NR!=1&&\\\$3=="0x2" {print \\\$4}' /proc/net/arp | tr "\\n" ",")
tmp='{"key":"{$setting['key']}","mac":"'\\\${mac}'","time":"'\\\$(date +%s)'"}'
curl -4 -X POST "{$reportUrl}" -H "Content-Type: application/json" -d \\\${tmp}
EOF
chmod +x /etc/init.d/dootask-checkin-report
crontab -l >/tmp/cronbak
sed -i '/\/etc\/init.d\/dootask-checkin-report/d' /tmp/cronbak
sed -i '/^$/d' /tmp/cronbak
echo "* * * * * sh /etc/init.d/dootask-checkin-report" >>/tmp/cronbak
crontab /tmp/cronbak
rm -f /tmp/cronbak
/etc/init.d/cron enable
/etc/init.d/cron restart
echo 'installed'
EOE;
}
/**
* {post} 签到 - 路由器openwrt上报
*
* @apiParam {String} key
* @apiParam {String} mac 使用逗号分割多个
* @apiParam {String} time
*
* @return string
*/
public function checkin__report()
{
$key = trim(Request::input('key'));
$mac = trim(Request::input('mac'));
$time = intval(Request::input('time'));
//
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return 'function off';
}
if ($key != $setting['key']) {
return 'key error';
}
$times = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00'];
$advance = (intval($setting['advance']) ?: 120) * 60;
$delay = (intval($setting['delay']) ?: 120) * 60;
//
$nowDate = date("Y-m-d");
$nowTime = date("H:i:s");
//
$timeStart = strtotime("{$nowDate} {$times[0]}");
$timeEnd = strtotime("{$nowDate} {$times[1]}");
$timeAdvance = max($timeStart - $advance, strtotime($nowDate));
$timeDelay = min($timeEnd + $delay, strtotime("{$nowDate} 23:59:59"));
if (Base::time() < $timeAdvance || $timeDelay < Base::time()) {
return "not in valid time, valid time is " . date("H:i", $timeAdvance) . "-" . date("H:i", $timeDelay);
}
//
$macs = explode(",", $mac);
$checkins = [];
foreach ($macs as $mac) {
$mac = strtoupper($mac);
if (Base::isMac($mac) && $UserCheckinMac = UserCheckinMac::whereMac($mac)->first()) {
$checkins[] = $UserCheckinMac;
$array = [
'userid' => $UserCheckinMac->userid,
'mac' => $UserCheckinMac->mac,
'date' => $nowDate,
];
$record = UserCheckinRecord::where($array)->first();
if (empty($record)) {
$record = UserCheckinRecord::createInstance($array);
}
$record->times = Base::array2json(array_merge($record->times, [$nowTime]));
$record->report_time = $time;
$record->save();
}
}
//
if ($checkins && $botUser = User::botGetOrCreate('check-in')) {
$getJokeSoup = function($type) {
$pre = $type == "up" ? "每日开心:" : "心灵鸡汤:";
$key = $type == "up" ? "JokeSoupTask:jokes" : "JokeSoupTask:soups";
$array = Base::json2array(Cache::get($key));
if ($array) {
$item = $array[array_rand($array)];
if ($item) {
return $pre . $item;
}
}
return null;
};
$sendMsg = function($type, UserCheckinMac $checkin) use ($getJokeSoup, $botUser, $nowDate) {
$cacheKey = "Checkin::sendMsg-{$nowDate}-{$type}:" . $checkin->userid;
if (Cache::get($cacheKey) === "yes") {
return;
}
Cache::put($cacheKey, "yes", Carbon::now()->addDay());
//
$dialog = WebSocketDialog::checkUserDialog($botUser, $checkin->userid);
if ($dialog) {
$hi = date("H:i");
$pre = $type == "up" ? "上班" : "下班";
$remark = $checkin->remark ? " ({$checkin->remark})": "";
$text = "<p>{$pre}打卡成功,打卡时间: {$hi}{$remark}</p>";
$suff = $getJokeSoup($type);
if ($suff) {
$text = "{$text}<p>----------</p><p>{$suff}</p>";
}
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid);
}
};
if ($timeAdvance <= Base::time() && Base::time() < $timeEnd) {
// 上班打卡通知(从最早打卡时间 到 下班打卡时间)
foreach ($checkins as $checkin) {
$sendMsg('up', $checkin);
}
}
if ($timeEnd <= Base::time() && Base::time() <= $timeDelay) {
// 下班打卡通知(下班打卡时间 到 最晚打卡时间)
foreach ($checkins as $checkin) {
$sendMsg('down', $checkin);
}
}
}
return 'success';
}
}

View File

@ -9,6 +9,7 @@ use App\Models\Report;
use App\Models\ReportReceive;
use App\Models\User;
use App\Module\Base;
use App\Module\Doo;
use App\Tasks\PushTask;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
@ -119,12 +120,13 @@ class ReportController extends AbstractController
* @apiGroup report
* @apiName store
*
* @apiParam {Number} [id] 汇报ID
* @apiParam {String} [title] 汇报标题
* @apiParam {Array} [type] 汇报类型weekly:周报daily:日报
* @apiParam {Number} [content] 内容
* @apiParam {Number} [receive] 汇报对象
* @apiParam {Number} [offset] 偏移量
* @apiParam {Number} id 汇报ID0为新建
* @apiParam {String} [sign] 唯一签名,通过[api/report/template]接口返回
* @apiParam {String} title 汇报标题
* @apiParam {Array} type 汇报类型weekly:周报daily:日报
* @apiParam {Number} content 内容
* @apiParam {Number} [receive] 汇报对象
* @apiParam {Number} offset 时间偏移量
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -132,8 +134,11 @@ class ReportController extends AbstractController
*/
public function store(): array
{
$user = User::auth();
//
$input = [
"id" => Base::getPostValue("id", 0),
"sign" => Base::getPostValue("sign"),
"title" => Base::getPostValue("title"),
"type" => Base::getPostValue("type"),
"content" => Base::getPostValue("content"),
@ -146,7 +151,6 @@ class ReportController extends AbstractController
'title' => 'required',
'type' => ['required', Rule::in([Report::WEEKLY, Report::DAILY])],
'content' => 'required',
'receive' => 'required',
'offset' => ['numeric', 'max:0'],
], [
'id.numeric' => 'ID只能是数字',
@ -154,14 +158,12 @@ class ReportController extends AbstractController
'type.required' => '请选择汇报类型',
'type.in' => '汇报类型错误',
'content.required' => '请填写汇报内容',
'receive.required' => '请选择接收人',
'offset.numeric' => '工作汇报周期格式错误,只能是数字',
'offset.max' => '只能提交当天/本周或者之前的的工作汇报',
]);
if ($validator->fails())
return Base::retError($validator->errors()->first());
$user = User::auth();
// 接收人
if (is_array($input["receive"])) {
// 删除当前登录人
@ -193,25 +195,24 @@ class ReportController extends AbstractController
]);
} else {
// 生成唯一标识
$sign = Report::generateSign($input["type"], $input["offset"]);
$sign = Base::isNumber($input["sign"]) ? $input["sign"] : Report::generateSign($input["type"], $input["offset"]);
// 检查唯一标识是否存在
if (empty($input["id"])) {
if (Report::query()->whereSign($sign)->whereType($input["type"])->count() > 0)
throw new ApiException("请勿重复提交工作汇报");
if (empty($input["id"]) && Report::query()->whereSign($sign)->whereType($input["type"])->count() > 0) {
throw new ApiException("请勿重复提交工作汇报");
}
$report = Report::createInstance([
"sign" => $sign,
"title" => $input["title"],
"type" => $input["type"],
"content" => htmlspecialchars($input["content"]),
"userid" => $user->userid,
"sign" => $sign,
"content" => htmlspecialchars($input["content"]),
]);
}
$report->save();
if (!empty($input["receive_content"])) {
// 删除关联
$report->Receives()->delete();
// 删除关联
$report->Receives()->delete();
if ($input["receive_content"]) {
// 保存接收人
$report->Receives()->createMany($input["receive_content"]);
}
@ -286,9 +287,10 @@ class ReportController extends AbstractController
// 如果已经提交了相关汇报
if ($one && $id > 0) {
return Base::retSuccess('success', [
"content" => $one->content,
"title" => $one->title,
"id" => $one->id,
"sign" => $one->sign,
"title" => $one->title,
"content" => $one->content,
]);
}
@ -305,8 +307,8 @@ class ReportController extends AbstractController
if ($complete_task->isNotEmpty()) {
foreach ($complete_task as $task) {
$complete_at = Carbon::parse($task->complete_at);
$pre = $type == Report::WEEKLY ? ('<span>[' . Base::Lang('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</span>&nbsp;') : '';
$completeContent .= '<li>' . $pre . $task->name . '</li>';
$pre = $type == Report::WEEKLY ? ('<span>[' . Doo::translate('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</span>&nbsp;') : '';
$completeContent .= "<li>{$pre}[{$task->project->name}] {$task->name}</li>";
}
} else {
$completeContent = '<li>&nbsp;</li>';
@ -326,8 +328,8 @@ class ReportController extends AbstractController
if ($unfinished_task->isNotEmpty()) {
foreach ($unfinished_task as $task) {
empty($task->end_at) || $end_at = Carbon::parse($task->end_at);
$pre = (!empty($end_at) && $end_at->lt($now_dt)) ? '<span style="color:#ff0000;">[' . Base::Lang('超期') . ']</span>&nbsp;' : '';
$unfinishedContent .= '<li>' . $pre . $task->name . '</li>';
$pre = (!empty($end_at) && $end_at->lt($now_dt)) ? '<span style="color:#ff0000;">[' . Doo::translate('超期') . ']</span>&nbsp;' : '';
$unfinishedContent .= "<li>{$pre}[{$task->project->name}] {$task->name}</li>";
}
} else {
$unfinishedContent = '<li>&nbsp;</li>';
@ -339,15 +341,21 @@ class ReportController extends AbstractController
} else {
$title = $user->nickname . "的日报[" . $start_time->format("Y/m/d") . "]";
}
// 生成内容
$content = '<h2>' . Doo::translate('已完成工作') . '</h2><ol>' .
$completeContent . '</ol><h2>' .
Doo::translate('未完成的工作') . '</h2><ol>' .
$unfinishedContent . '</ol>';
if ($type === Report::WEEKLY) {
$content .= "<h2>" . Doo::translate("下周拟定计划") . "[" . $start_time->addWeek()->format("m/d") . "-" . $end_time->addWeek()->format("m/d") . "]</h2><ol><li>&nbsp;</li></ol>";
}
$data = [
"time" => $start_time->toDateTimeString(),
"sign" => $sign,
"title" => $title,
"content" => $content,
"complete_task" => $complete_task,
"unfinished_task" => $unfinished_task,
"content" => '<h2>' . Base::Lang('已完成工作') . '</h2><ol>' .
$completeContent . '</ol><h2>' .
Base::Lang('未完成的工作') . '</h2><ol>' .
$unfinishedContent . '</ol>',
"title" => $title,
];
if ($one) {
$data['id'] = $one->id;
@ -393,7 +401,49 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/last_submitter 06. 获取最后一次提交的接收人
* @api {get} api/report/mark 06. 标记已读/未读
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName mark
*
* @apiParam {Number} id 报告id
* @apiParam {Number} action 操作
* - read: 标记已读(默认)
* - unread: 标记未读
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function mark(): array
{
$user = User::auth();
//
$id = Request::input('id');
$action = Request::input('action');
//
if (is_array($id)) {
if (count(Base::arrayRetainInt($id)) > 100) {
return Base::retError("最多只能操作100条数据");
}
$builder = Report::whereIn("id", Base::arrayRetainInt($id));
} else {
$builder = Report::whereId(intval($id));
}
$builder ->chunkById(100, function ($list) use ($action, $user) {
/** @var Report $item */
foreach ($list as $item) {
$item->receivesUser()->updateExistingPivot($user->userid, [
"read" => $action === 'unread' ? 0 : 1,
]);
}
});
return Base::retSuccess("操作成功");
}
/**
* @api {get} api/report/last_submitter 07. 获取最后一次提交的接收人
*
* @apiVersion 1.0.0
* @apiGroup report
@ -410,23 +460,20 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/unread 07. 获取未读
* @api {get} api/report/unread 08. 获取未读
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName unread
*
* @apiParam {Number} [userid] 用户id
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function unread(): array
{
$userid = intval(trim(Request::input("userid")));
$user = empty($userid) ? User::auth() : User::find($userid);
$user = User::auth();
//
$data = Report::whereHas("Receives", function (Builder $query) use ($user) {
$query->where("userid", $user->userid)->where("read", 0);
})->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
@ -434,7 +481,7 @@ class ReportController extends AbstractController
}
/**
* @api {get} api/report/read 08. 标记汇报已读,可批量
* @api {get} api/report/read 09. 标记汇报已读,可批量
*
* @apiVersion 1.0.0
* @apiGroup report
@ -455,7 +502,7 @@ class ReportController extends AbstractController
}
if (is_string($ids)) {
$ids = explode(",", $ids);
$ids = Base::explodeInt($ids);
}
$data = Report::with(["receivesUser" => function (BelongsToMany $query) use ($user) {

View File

@ -2,12 +2,24 @@
namespace App\Http\Controllers\Api;
use App\Models\Setting;
use App\Models\User;
use App\Models\UserCheckinRecord;
use App\Module\Base;
use App\Module\BillExport;
use App\Module\BillMultipleExport;
use App\Module\Doo;
use App\Module\Extranet;
use Arr;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
use LdapRecord\Container;
use LdapRecord\LdapRecordException;
use Madzipper;
use Request;
use Response;
use Session;
/**
* @apiDefine system
@ -27,7 +39,7 @@ class SystemController extends AbstractController
* @apiParam {String} type
* - get: 获取(默认)
* - all: 获取所有(需要管理员权限)
* - save: 保存设置(参数:['reg', 'reg_invite', 'login_code', 'password_policy', 'project_invite', 'chat_nickname', 'auto_archived', 'archived_day', 'start_home', 'home_footer']
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'auto_archived', 'archived_day', 'all_group_mute', 'all_group_autoin', 'start_home', 'home_footer']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -45,13 +57,17 @@ class SystemController extends AbstractController
foreach ($all AS $key => $value) {
if (!in_array($key, [
'reg',
'reg_identity',
'reg_invite',
'login_code',
'password_policy',
'project_invite',
'chat_nickname',
'chat_information',
'anon_message',
'auto_archived',
'archived_day',
'all_group_mute',
'all_group_autoin',
'start_home',
'home_footer'
])) {
@ -79,12 +95,16 @@ class SystemController extends AbstractController
}
//
$setting['reg'] = $setting['reg'] ?: 'open';
$setting['reg_identity'] = $setting['reg_identity'] ?: 'normal';
$setting['login_code'] = $setting['login_code'] ?: 'auto';
$setting['password_policy'] = $setting['password_policy'] ?: 'simple';
$setting['project_invite'] = $setting['project_invite'] ?: 'open';
$setting['chat_nickname'] = $setting['chat_nickname'] ?: 'optional';
$setting['chat_information'] = $setting['chat_information'] ?: 'optional';
$setting['anon_message'] = $setting['anon_message'] ?: 'open';
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
$setting['all_group_mute'] = $setting['all_group_mute'] ?: 'open';
$setting['all_group_autoin'] = $setting['all_group_autoin'] ?: 'yes';
$setting['start_home'] = $setting['start_home'] ?: 'close';
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
@ -99,20 +119,21 @@ class SystemController extends AbstractController
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:['smtp_server', 'port', 'account', 'password', 'reg_verify', 'notice', 'task_remind_hours', 'task_remind_hours2']
* - save: 保存设置(参数:['smtp_server', 'port', 'account', 'password', 'reg_verify', 'notice_msg', 'msg_unread_user_minute', 'msg_unread_group_minute', 'ignore_addr']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__email()
{
User::auth('admin');
$user = User::auth();
//
$type = trim(Request::input('type'));
if ($type == 'save') {
if (env("SYSTEM_SETTING") == 'disabled') {
return Base::retError('当前环境禁止修改');
}
$user->identity('admin');
$all = Request::input();
foreach ($all as $key => $value) {
if (!in_array($key, [
@ -121,9 +142,10 @@ class SystemController extends AbstractController
'account',
'password',
'reg_verify',
'notice',
'task_remind_hours',
'task_remind_hours2'
'notice_msg',
'msg_unread_user_minute',
'msg_unread_group_minute',
'ignore_addr'
])) {
unset($all[$key]);
}
@ -138,15 +160,241 @@ class SystemController extends AbstractController
$setting['account'] = $setting['account'] ?: '';
$setting['password'] = $setting['password'] ?: '';
$setting['reg_verify'] = $setting['reg_verify'] ?: 'close';
$setting['notice'] = $setting['notice'] ?: 'open';
$setting['task_remind_hours'] = floatval($setting['task_remind_hours']) ?: 0;
$setting['task_remind_hours2'] = floatval($setting['task_remind_hours2']) ?: 0;
$setting['notice_msg'] = $setting['notice_msg'] ?: 'close';
$setting['msg_unread_user_minute'] = intval($setting['msg_unread_user_minute'] ?? -1);
$setting['msg_unread_group_minute'] = intval($setting['msg_unread_group_minute'] ?? -1);
$setting['ignore_addr'] = $setting['ignore_addr'] ?: '';
//
if ($type != 'save' && !in_array('admin', $user->identity)) {
$setting = array_intersect_key($setting, array_flip(['reg_verify']));
}
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/demo 03. 获取演示账号
* @api {get} api/system/setting/meeting 03. 获取会议设置、保存会议设置(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName setting__meeting
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:['open', 'appid', 'app_certificate']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__meeting()
{
User::auth('admin');
//
$type = trim(Request::input('type'));
if ($type == 'save') {
if (env("SYSTEM_SETTING") == 'disabled') {
return Base::retError('当前环境禁止修改');
}
$all = Request::input();
foreach ($all as $key => $value) {
if (!in_array($key, [
'open',
'appid',
'app_certificate',
])) {
unset($all[$key]);
}
}
$setting = Base::setting('meetingSetting', Base::newTrim($all));
} else {
$setting = Base::setting('meetingSetting');
}
//
$setting['open'] = $setting['open'] ?: 'close';
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/setting/checkin 04. 获取签到设置、保存签到设置(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName setting__checkin
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:['open', 'time', 'advance', 'delay', 'remindin', 'remindexceed', 'edit', 'key']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__checkin()
{
User::auth('admin');
//
$type = trim(Request::input('type'));
if ($type == 'save') {
if (env("SYSTEM_SETTING") == 'disabled') {
return Base::retError('当前环境禁止修改');
}
$all = Request::input();
foreach ($all as $key => $value) {
if (!in_array($key, [
'open',
'time',
'advance',
'delay',
'remindin',
'remindexceed',
'edit',
'key',
])) {
unset($all[$key]);
}
}
if ($all['open'] === 'close') {
$all['key'] = md5(Base::generatePassword(32));
}
$setting = Base::setting('checkinSetting', Base::newTrim($all));
} else {
$setting = Base::setting('checkinSetting');
}
//
if (empty($setting['key'])) {
$setting['key'] = md5(Base::generatePassword(32));
Base::setting('checkinSetting', $setting);
}
//
$setting['open'] = $setting['open'] ?: 'close';
$setting['time'] = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00'];
$setting['advance'] = intval($setting['advance']) ?: 120;
$setting['delay'] = intval($setting['delay']) ?: 120;
$setting['remindin'] = intval($setting['remindin']) ?: 5;
$setting['remindexceed'] = intval($setting['remindexceed']) ?: 10;
$setting['edit'] = $setting['edit'] ?: 'close';
$setting['cmd'] = "curl -sSL '" . Base::fillUrl("api/public/checkin/install?key={$setting['key']}") . "' | sh";
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/setting/apppush 05. 获取APP推送设置、保存APP推送设置限管理员
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName setting__apppush
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:['push', 'ios_key', 'ios_secret', 'android_key', 'android_secret']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__apppush()
{
User::auth('admin');
//
$type = trim(Request::input('type'));
if ($type == 'save') {
if (env("SYSTEM_SETTING") == 'disabled') {
return Base::retError('当前环境禁止修改');
}
$all = Request::input();
foreach ($all as $key => $value) {
if (!in_array($key, [
'push',
'ios_key',
'ios_secret',
'android_key',
'android_secret',
])) {
unset($all[$key]);
}
}
$setting = Base::setting('appPushSetting', Base::newTrim($all));
} else {
$setting = Base::setting('appPushSetting');
}
//
$setting['push'] = $setting['push'] ?: 'close';
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/setting/thirdaccess 06. 第三方帐号(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName setting__thirdaccess
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:['ldap_open', 'ldap_host', 'ldap_port', 'ldap_password', 'ldap_user_dn', 'ldap_base_dn', 'ldap_sync_local']
* - testldap: 测试ldap连接
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__thirdaccess()
{
User::auth('admin');
//
$type = trim(Request::input('type'));
if ($type == 'testldap') {
$all = Base::newTrim(Request::input());
$connection = Container::getDefaultConnection();
try {
$connection->setConfiguration([
"hosts" => [$all['ldap_host']],
"port" => intval($all['ldap_port']),
"password" => $all['ldap_password'],
"username" => $all['ldap_user_dn'],
"base_dn" => $all['ldap_base_dn'],
]);
if ($connection->auth()->attempt($all['ldap_user_dn'], $all['ldap_password'])) {
return Base::retSuccess('验证通过');
} else {
return Base::retError('验证失败');
}
} catch (LdapRecordException $e) {
return Base::retError($e->getMessage() ?: "验证失败:未知错误", config("ldap.connections.default"));
}
} elseif ($type == 'save') {
if (env("SYSTEM_SETTING") == 'disabled') {
return Base::retError('当前环境禁止修改');
}
$all = Base::newTrim(Request::input());
foreach ($all as $key => $value) {
if (!in_array($key, [
'ldap_open',
'ldap_host',
'ldap_port',
'ldap_password',
'ldap_user_dn',
'ldap_base_dn',
'ldap_sync_local'
])) {
unset($all[$key]);
}
}
$all['ldap_port'] = intval($all['ldap_port']) ?: 389;
$setting = Base::setting('thirdAccessSetting', Base::newTrim($all));
} else {
$setting = Base::setting('thirdAccessSetting');
}
//
$setting['ldap_open'] = $setting['ldap_open'] ?: 'close';
$setting['ldap_port'] = intval($setting['ldap_port']) ?: 389;
$setting['ldap_sync_local'] = $setting['ldap_sync_local'] ?: 'close';
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/demo 07. 获取演示帐号
*
* @apiVersion 1.0.0
* @apiGroup system
@ -170,7 +418,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/priority 04. 任务优先级
* @api {post} api/system/priority 08. 任务优先级
*
* @apiDescription 获取任务优先级、保存任务优先级
* @apiVersion 1.0.0
@ -219,7 +467,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/column/template 05. 创建项目模板
* @api {post} api/system/column/template 09. 创建项目模板
*
* @apiDescription 获取创建项目模板、保存创建项目模板
* @apiVersion 1.0.0
@ -266,7 +514,43 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/info 06. 获取终端详细信息
* @api {post} api/system/license 08. License
*
* @apiDescription 获取License信息、保存License限管理员
* @apiVersion 1.0.0
* @apiGroup system
* @apiName license
*
* @apiParam {String} type
* - get: 获取
* - save: 保存
* @apiParam {String} license License 原文
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function license()
{
User::auth('admin');
//
$type = trim(Request::input('type'));
if ($type == 'save') {
$license = Base::getPostValue('license');
Doo::licenseSave($license);
}
//
return Base::retSuccess('success', [
'license' => Doo::licenseContent(),
'info' => Doo::license(),
'macs' => Doo::macs(),
'doo_sn' => Doo::dooSN(),
'user_count' => User::whereBot(0)->whereNull('disable_at')->count(),
]);
}
/**
* @api {get} api/system/get/info 10. 获取终端详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@ -285,17 +569,17 @@ class SystemController extends AbstractController
}
return Base::retSuccess('success', [
'ip' => Base::getIp(),
'ip-info' => Base::getIpInfo(Base::getIp()),
'ip-gcj02' => Base::getIpGcj02(Base::getIp()),
'ip-info' => Extranet::getIpInfo(Base::getIp()),
'ip-gcj02' => Extranet::getIpGcj02(Base::getIp()),
'ip-iscn' => Base::isCnIp(Base::getIp()),
'header' => Request::header(),
'token' => Base::getToken(),
'token' => Doo::userToken(),
'url' => url('') . Base::getUrl(),
]);
}
/**
* @api {get} api/system/get/ip 07. 获取IP地址
* @api {get} api/system/get/ip 11. 获取IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@ -310,7 +594,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/cnip 08. 是否中国IP地址
* @api {get} api/system/get/cnip 12. 是否中国IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@ -327,7 +611,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipgcj02 09. 获取IP地址经纬度
* @api {get} api/system/get/ipgcj02 13. 获取IP地址经纬度
*
* @apiVersion 1.0.0
* @apiGroup system
@ -340,11 +624,11 @@ class SystemController extends AbstractController
* @apiSuccess {Object} data 返回数据
*/
public function get__ipgcj02() {
return Base::getIpGcj02(Request::input("ip"));
return Extranet::getIpGcj02(Request::input("ip"));
}
/**
* @api {get} api/system/get/ipinfo 10. 获取IP地址详细信息
* @api {get} api/system/get/ipinfo 14. 获取IP地址详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@ -357,19 +641,26 @@ class SystemController extends AbstractController
* @apiSuccess {Object} data 返回数据
*/
public function get__ipinfo() {
return Base::getIpInfo(Request::input("ip"));
return Extranet::getIpInfo(Request::input("ip"));
}
/**
* @api {post} api/system/imgupload 11. 上传图片
* @api {post} api/system/imgupload 15. 上传图片
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup system
* @apiName imgupload
*
* @apiParam {String} image64 图片base64
* @apiParam {String} filename 文件名
* @apiParam {File} image post-图片对象
* @apiParam {String} [image64] post-图片base64'image'二选一)
* @apiParam {String} filename post-文件名
* @apiParam {Number} [width] 压缩图片宽默认0
* @apiParam {Number} [height] 压缩图片高默认0
* @apiParam {String} [whcut] 压缩方式
* - 1裁切默认宽、高非0有效
* - 0:缩放
* - -1'auto':保持等比裁切
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -380,9 +671,12 @@ class SystemController extends AbstractController
if (User::userid() === 0) {
return Base::retError('身份失效,等重新登录');
}
$scale = [intval(Request::input('width')), intval(Request::input('height'))];
if (!$scale[0] && !$scale[1]) {
$scale = [2160, 4160, -1];
$width = intval(Request::input('width'));
$height = intval(Request::input('height'));
$whcut = intval(Request::input('whcut', 1));
$scale = [2160, 4160, -1];
if ($width > 0 || $height > 0) {
$scale = [$width, $height, $whcut];
}
$path = "uploads/user/picture/" . User::userid() . "/" . date("Ym") . "/";
$image64 = trim(Base::getPostValue('image64'));
@ -411,7 +705,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/imgview 12. 浏览图片空间
* @api {get} api/system/get/imgview 16. 浏览图片空间
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -466,7 +760,7 @@ class SystemController extends AbstractController
'path' => $pathTemp,
'url' => Base::fillUrl($pathTemp),
'thumb' => Base::fillUrl('images/other/dir.png'),
'inode' => fileatime($v),
'inode' => filemtime($v),
];
} elseif (!str_ends_with($filename, "_thumb.jpg")) {
$array = [
@ -475,7 +769,7 @@ class SystemController extends AbstractController
'path' => $pathTemp,
'url' => Base::fillUrl($pathTemp),
'thumb' => $pathTemp,
'inode' => fileatime($v),
'inode' => filemtime($v),
];
//
$extension = pathinfo($dirPath . $filename, PATHINFO_EXTENSION);
@ -507,7 +801,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/fileupload 13. 上传文件
* @api {post} api/system/fileupload 17. 上传文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@ -549,7 +843,39 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/starthome 14. 启动首页设置信息
* @api {get} api/system/get/showitem 18. 首页显示ITEM
*
* @apiDescription 用于判断首页是否显示pro、github、更新日志...
* @apiVersion 1.0.0
* @apiGroup system
* @apiName get__showitem
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function get__showitem()
{
$logPath = base_path('CHANGELOG.md');
$logContent = "";
$logVersion = "";
if (file_exists($logPath)) {
$logContent = file_get_contents($logPath);
preg_match("/## \[(.*?)\]/", $logContent, $matchs);
if ($matchs) {
$logVersion = $matchs[1] === "Unreleased" ? $matchs[1] : "v{$matchs[1]}";
}
}
return Base::retSuccess('success', [
'pro' => str_contains(Request::getHost(), "dootask.com") || str_contains(Request::getHost(), "127.0.0.1"),
'github' => env('GITHUB_URL') ?: false,
'updateLog' => $logContent ?: false,
'updateVer' => $logVersion,
]);
}
/**
* @api {get} api/system/get/starthome 19. 启动首页设置信息
*
* @apiDescription 用于判断注册是否需要启动首页
* @apiVersion 1.0.0
@ -569,7 +895,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/email/check 15. 邮件发送测试(限管理员)
* @api {get} api/system/email/check 20. 邮件发送测试(限管理员)
*
* @apiDescription 测试配置邮箱是否能发送邮件
* @apiVersion 1.0.0
@ -589,16 +915,20 @@ class SystemController extends AbstractController
return Base::retError('请输入正确的收件人地址');
}
try {
Factory::mailer()
->setDsn("smtp://{$all['account']}:{$all['password']}@{$all['smtp_server']}:{$all['port']}?verify_peer=0")
->setMessage(EmailMessage::create()
->from(env('APP_NAME', 'Task') . " <{$all['account']}>")
->to($all['to'])
->subject('Mail sending test')
->html('<p>收到此电子邮件意味着您的邮箱配置正确。</p><p>Receiving this email means that your mailbox is configured correctly.</p>'))
->send();
Setting::validateAddr($all['to'], function($to) use ($all) {
Factory::mailer()
->setDsn("smtp://{$all['account']}:{$all['password']}@{$all['smtp_server']}:{$all['port']}?verify_peer=0")
->setMessage(EmailMessage::create()
->from(env('APP_NAME', 'Task') . " <{$all['account']}>")
->to($to)
->subject('Mail sending test')
->html('<p>收到此电子邮件意味着您的邮箱配置正确。</p><p>Receiving this email means that your mailbox is configured correctly.</p>'))
->send();
}, function () {
throw new \Exception("收件人地址错误或已被忽略");
});
return Base::retSuccess('成功发送');
} catch (\Exception $e) {
} catch (\Throwable $e) {
// 一般是请求超时
if (str_contains($e->getMessage(), "Timed Out")) {
return Base::retError("language.TimedOut");
@ -609,4 +939,228 @@ class SystemController extends AbstractController
}
}
}
/**
* @api {get} api/system/checkin/export 21. 导出签到数据(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName checkin__export
*
* @apiParam {Array} [userid] 指定会员,如:[1, 2]
* @apiParam {Array} [date] 指定日期范围,如:['2020-12-12', '2020-12-30']
* @apiParam {Array} [time] 指定时间范围,如:['09:00', '18:00']
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function checkin__export()
{
User::auth('admin');
//
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return Base::retError('此功能未开启,请前往系统设置开启');
}
//
$userid = Base::arrayRetainInt(Request::input('userid'), true);
$date = Request::input('date');
$time = Request::input('time');
//
if (empty($userid) || empty($date) || empty($time)) {
return Base::retError('参数错误');
}
if (count($userid) > 100) {
return Base::retError('导出成员限制最多100个');
}
if (!(is_array($date) && Base::isDate($date[0]) && Base::isDate($date[1]))) {
return Base::retError('日期选择错误');
}
if (Carbon::parse($date[1])->timestamp - Carbon::parse($date[0])->timestamp > 35 * 86400) {
return Base::retError('日期范围限制最大35天');
}
if (!(is_array($time) && Base::isTime($time[0]) && Base::isTime($time[1]))) {
return Base::retError('时间选择错误');
}
//
$secondStart = strtotime("2000-01-01 {$time[0]}") - strtotime("2000-01-01 00:00:00");
$secondEnd = strtotime("2000-01-01 {$time[1]}") - strtotime("2000-01-01 00:00:00");
//
$headings = [];
$headings[] = '签到人';
$headings[] = '签到日期';
$headings[] = '班次时间';
$headings[] = '首次签到时间';
$headings[] = '首次签到结果';
$headings[] = '最后签到时间';
$headings[] = '最后签到结果';
$headings[] = '参数数据';
//
$sheets = [];
$startD = Carbon::parse($date[0])->startOfDay();
$endD = Carbon::parse($date[1])->endOfDay();
$users = User::whereIn('userid', $userid)->take(100)->get();
/** @var User $user */
foreach ($users as $user) {
$recordTimes = UserCheckinRecord::getTimes($user->userid, [$startD, $endD]);
//
$nickname = Base::filterEmoji($user->nickname);
$styles = ["A1:H1" => ["font" => ["bold" => true]]];
$datas = [];
$startT = $startD->timestamp;
$endT = $endD->timestamp;
$index = 1;
while ($startT < $endT) {
$index++;
$sameDate = date("Y-m-d", $startT);
$sameTimes = $recordTimes[$sameDate] ?? [];
$sameCollect = UserCheckinRecord::atCollect($sameDate, $sameTimes);
$firstBetween = [Carbon::createFromTimestamp($startT), Carbon::createFromTimestamp($startT + $secondEnd - 1)];
$lastBetween = [Carbon::createFromTimestamp($startT + $secondStart + 1), Carbon::createFromTimestamp($startT + 86400)];
$firstRecord = $sameCollect?->whereBetween("datetime", $firstBetween)->first();
$lastRecord = $sameCollect?->whereBetween("datetime", $lastBetween)->last();
$firstTimestamp = $firstRecord['timestamp'] ?: 0;
$lastTimestamp = $lastRecord['timestamp'] ?: 0;
if (Base::time() < $startT + $secondStart) {
$firstResult = "-";
} else {
$firstResult = "正常";
if (empty($firstTimestamp)) {
$firstResult = "缺卡";
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]];
} elseif ($firstTimestamp > $startT + $secondStart) {
$firstResult = "迟到";
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
}
}
if (Base::time() < $startT + $secondEnd) {
$lastResult = "-";
$lastTimestamp = 0;
} else {
$lastResult = "正常";
if (empty($lastTimestamp) || $lastTimestamp === $firstTimestamp) {
$lastResult = "缺卡";
$styles["G{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]];
} elseif ($lastTimestamp < $startT + $secondEnd) {
$lastResult = "早退";
$styles["G{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
}
}
$firstTimestamp = $firstTimestamp ? date("H:i", $firstTimestamp) : "-";
$lastTimestamp = $lastTimestamp ? date("H:i", $lastTimestamp) : "-";
$section = array_map(function($item) {
return $item[0] . "-" . ($item[1] ?: "None");
}, UserCheckinRecord::atSection($sameTimes));
$datas[] = [
"{$nickname} (ID: {$user->userid})",
$sameDate,
implode("-", $time),
$firstTimestamp,
$firstResult,
$lastTimestamp,
$lastResult,
implode(", ", $section),
];
$startT += 86400;
}
$title = (count($sheets) + 1) . "." . ($nickname ?: $user->userid);
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles($styles);
}
if (empty($sheets)) {
return Base::retError('没有任何数据');
}
//
$fileName = $users[0]->nickname;
if (count($users) > 1) {
$fileName .= "" . count($userid) . "位成员";
}
$fileName .= '签到记录_' . Base::time() . '.xls';
$filePath = "temp/checkin/export/" . date("Ym", Base::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
return Base::retError('导出失败,' . $fileName . '');
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Throwable) {
}
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
Session::put('checkin::export:userid', $user->userid);
return Base::retSuccess('success', [
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
'url' => Base::fillUrl('api/system/checkin/down?key=' . urlencode($base64)),
]);
} else {
return Base::retError('打包失败,请稍后再试...');
}
}
/**
* @api {get} api/system/checkin/down 22. 下载导出的签到数据
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName checkin__down
*
* @apiParam {String} key 通过export接口得到的下载钥匙
*
* @apiSuccess {File} data 返回数据(直接下载文件)
*/
public function checkin__down()
{
$userid = Session::get('checkin::export:userid');
if (empty($userid)) {
return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502);
}
//
$array = Base::string2array(base64_decode(urldecode(Request::input('key'))));
$file = $array['file'];
if (empty($file) || !file_exists(storage_path($file))) {
return Base::ajaxError("文件不存在!", [], 0, 502);
}
return Response::download(storage_path($file));
}
/**
* @api {get} api/system/version 23. 获取版本号
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName version
*
* @apiSuccess {String} version
* @apiSuccess {String} publish
*/
public function version()
{
$url = url('');
$package = Base::getPackage();
$array = [
'version' => Base::getVersion(),
'publish' => [],
];
if (is_array($package['app'])) {
$i = 0;
foreach ($package['app'] as $item) {
$urls = $item['urls'] && is_array($item['urls']) ? $item['urls'] : $item['url'];
if (is_array($item['publish']) && ($i === 0 || Base::hostContrast($url, $urls))) {
$array['publish'] = $item['publish'];
}
$i++;
}
}
return $array;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,14 +2,25 @@
namespace App\Http\Controllers;
use App\Models\File;
use App\Module\Base;
use App\Module\Extranet;
use App\Module\RandomColor;
use App\Tasks\AppPushTask;
use App\Tasks\AutoArchivedTask;
use App\Tasks\CheckinRemindTask;
use App\Tasks\DeleteBotMsgTask;
use App\Tasks\DeleteTmpTask;
use App\Tasks\OverdueRemindEmailTask;
use App\Tasks\EmailNoticeTask;
use App\Tasks\JokeSoupTask;
use App\Tasks\LoopTask;
use Arr;
use Cache;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use LasseRafn\InitialAvatarGenerator\InitialAvatar;
use Redirect;
use Request;
use Response;
/**
@ -25,6 +36,10 @@ class IndexController extends InvokeController
if ($action) {
$app .= "__" . $action;
}
if ($app === 'manifest.txt') {
$app = 'manifest';
$child = 'txt';
}
if (!method_exists($this, $app)) {
$app = method_exists($this, $method) ? $method : 'main';
}
@ -33,45 +48,121 @@ class IndexController extends InvokeController
/**
* 首页
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @return \Illuminate\Http\Response
*/
public function main()
{
$hash = 'no';
$path = public_path('js/hash');
if (file_exists($path)) {
$hash = trim(file_get_contents(public_path('js/hash')));
if (strlen($hash) > 16) {
$hash = 'long';
}
$hotFile = public_path('hot');
$manifestFile = public_path('manifest.json');
if (file_exists($hotFile)) {
$array = Base::json2array(file_get_contents($hotFile));
$style = null;
$script = preg_replace("/^(\/\/(.*?))(:\d+)?\//i", "$1:" . $array['APP_DEV_PORT'] . "/", asset_main("resources/assets/js/app.js"));
} else {
$array = Base::json2array(file_get_contents($manifestFile));
$style = asset_main($array['resources/assets/js/app.js']['css'][0]);
$script = asset_main($array['resources/assets/js/app.js']['file']);
}
return view('main', [
return response()->view('main', [
'version' => Base::getVersion(),
'hash' => $hash
]);
'style' => $style,
'script' => $script,
])->header('Link', "<" . url('manifest.txt') . ">; rel=\"prefetch\"");
}
/**
* Manifest
* @param $child
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|string
*/
public function manifest($child = '')
{
if (empty($child)) {
$murl = url('manifest.txt');
return response($murl)->header('Link', "<{$murl}>; rel=\"prefetch\"");
}
$array = [
"office/web-apps/apps/api/documents/api.js?hash=" . Base::getVersion(),
"office/7.3.2-8/web-apps/vendor/requirejs/require.js",
"office/7.3.2-8/web-apps/apps/api/documents/api.js",
"office/7.3.2-8/sdkjs/common/AllFonts.js",
"office/7.3.2-8/web-apps/vendor/xregexp/xregexp-all-min.js",
"office/7.3.2-8/web-apps/vendor/sockjs/sockjs.min.js",
"office/7.3.2-8/web-apps/vendor/jszip/jszip.min.js",
"office/7.3.2-8/web-apps/vendor/jszip-utils/jszip-utils.min.js",
"office/7.3.2-8/sdkjs/common/libfont/wasm/fonts.js",
"office/7.3.2-8/sdkjs/common/Charts/ChartStyles.js",
"office/7.3.2-8/sdkjs/slide/themes//themes.js",
"office/7.3.2-8/web-apps/apps/presentationeditor/main/app.js",
"office/7.3.2-8/sdkjs/slide/sdk-all-min.js",
"office/7.3.2-8/sdkjs/slide/sdk-all.js",
"office/7.3.2-8/web-apps/apps/documenteditor/main/app.js",
"office/7.3.2-8/sdkjs/word/sdk-all-min.js",
"office/7.3.2-8/sdkjs/word/sdk-all.js",
"office/7.3.2-8/web-apps/apps/spreadsheeteditor/main/app.js",
"office/7.3.2-8/sdkjs/cell/sdk-all-min.js",
"office/7.3.2-8/sdkjs/cell/sdk-all.js",
];
foreach ($array as &$item) {
$item = url($item);
}
return implode(PHP_EOL, $array);
}
/**
* 获取版本号
* @return array
* @return \Illuminate\Http\RedirectResponse
*/
public function version()
{
$url = url('');
$package = Base::getPackage();
$array = [
'version' => Base::getVersion(),
'publish' => Arr::get($package, 'app.0.publish'),
];
if (is_array($package['app'])) {
foreach ($package['app'] as $item) {
if (is_array($item['publish']) && Base::hostContrast($url, $item['url'])) {
$array['publish'] = $item['publish'];
break;
}
}
return Redirect::to(Base::fillUrl('api/system/version'), 301);
}
/**
* 头像
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
*/
public function avatar()
{
$segment = Request::segment(2);
if ($segment && preg_match('/.*?\.png$/i', $segment)) {
$name = substr($segment, 0, -4);
} else {
$name = Request::input('name', 'H');
}
return $array;
$size = Request::input('size', 128);
$color = Request::input('color');
$background = Request::input('background');
//
if (preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', $name)) {
$name = mb_substr($name, mb_strlen($name) - 2);
}
if (empty($color)) {
$color = '#ffffff';
$cacheKey = "avatarBackgroundColor::" . md5($name);
$background = Cache::rememberForever($cacheKey, function() {
return RandomColor::one(['luminosity' => 'dark']);
});
}
//
$avatar = new InitialAvatar();
$content = $avatar->name($name)
->size($size)
->color($color)
->background($background)
->fontSize(0.35)
->autoFont()
->generate()
->stream('png', 100);
//
return response($content)
->header('Pragma', 'public')
->header('Cache-Control', 'max-age=1814400')
->header('Content-type', 'image/png')
->header('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400));
}
/**
@ -93,13 +184,24 @@ class IndexController extends InvokeController
// 限制内网访问
return "Forbidden Access";
}
// 自动归档
Task::deliver(new AutoArchivedTask());
// 邮件通知
Task::deliver(new EmailNoticeTask());
// App推送
Task::deliver(new AppPushTask());
// 删除过期的临时表数据
Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1));
Task::deliver(new DeleteTmpTask('task_worker', 12));
Task::deliver(new DeleteTmpTask('tmp', 24));
// 自动归档任务
Task::deliver(new AutoArchivedTask());
// 任务到期邮件提醒
Task::deliver(new OverdueRemindEmailTask());
// 删除机器人消息
Task::deliver(new DeleteBotMsgTask());
// 周期任务
Task::deliver(new LoopTask());
// 签到提醒
Task::deliver(new CheckinRemindTask());
// 获取笑话/心灵鸡汤
Task::deliver(new JokeSoupTask());
return "success";
}
@ -109,21 +211,31 @@ class IndexController extends InvokeController
*/
public function desktop__publish($name = '')
{
$publishVersion = Request::header('publish-version');
$latestFile = public_path("uploads/desktop/latest");
$genericVersion = Request::header('generic-version');
$latestVersion = file_exists($latestFile) ? trim(file_get_contents($latestFile)) : "0.0.1";
if (strtolower($name) === 'latest') {
$name = $latestVersion;
}
// 上传
if (preg_match("/^\d+\.\d+\.\d+$/", $genericVersion)) {
$genericPath = "uploads/desktop/" . $genericVersion . "/";
$res = Base::upload([
"file" => Request::file('file'),
"type" => 'desktop',
"path" => $genericPath,
"fileName" => true
]);
if (Base::isSuccess($res)) {
file_put_contents($latestFile, $genericVersion);
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
$publishKey = Request::header('publish-key');
if ($publishKey !== env('APP_KEY')) {
return Base::retError("key error");
}
if (version_compare($publishVersion, $latestVersion) > -1) { // 限制上传版本必须 ≥ 当前版本
$publishPath = "uploads/desktop/{$publishVersion}/";
$res = Base::upload([
"file" => Request::file('file'),
"type" => 'desktop',
"path" => $publishPath,
"fileName" => true
]);
if (Base::isSuccess($res)) {
file_put_contents($latestFile, $publishVersion);
}
return $res;
}
return $res;
}
// 列表
if (preg_match("/^\d+\.\d+\.\d+$/", $name)) {
@ -132,29 +244,119 @@ class IndexController extends InvokeController
$lists = Base::readDir($dirPath);
$files = [];
foreach ($lists as $file) {
if (str_ends_with($file, '.yml') || str_ends_with($file, '.yaml')) {
continue;
}
$fileName = Base::leftDelete($file, $dirPath);
$files[] = [
'name' => substr($fileName, 1),
'time' => date("Y-m-d H:i:s", fileatime($file)),
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path . $fileName),
];
}
//
$path = "uploads/android";
$dirPath = public_path($path);
$lists = Base::readDir($dirPath);
$apkFile = null;
foreach ($lists as $file) {
if (!str_ends_with($file, '.apk')) {
continue;
}
if ($apkFile && strtotime($apkFile['time']) > filemtime($file)) {
continue;
}
$fileName = Base::leftDelete($file, $dirPath);
$apkFile = [
'name' => substr($fileName, 1),
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path . $fileName),
];
}
if ($apkFile) {
$files = array_merge([$apkFile], $files);
}
return view('desktop', ['version' => $name, 'files' => $files]);
}
// 下载
if ($name && file_exists($latestFile)) {
$genericVersion = file_get_contents($latestFile);
if (preg_match("/^\d+\.\d+\.\d+$/", $genericVersion)) {
$filePath = public_path("uploads/desktop/{$genericVersion}/{$name}");
$publishVersion = file_get_contents($latestFile);
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
$filePath = public_path("uploads/desktop/{$publishVersion}/{$name}");
if (file_exists($filePath)) {
return response()->download($filePath);
return Response::download($filePath);
}
}
}
return abort(404);
}
/**
* Drawio 图标搜索
* @return array|mixed
*/
public function drawio__iconsearch()
{
$query = trim(Request::input('q'));
$page = trim(Request::input('p'));
$size = trim(Request::input('c'));
return Extranet::drawioIconSearch($query, $page, $size);
}
/**
* 预览文件
* @return array|mixed
*/
public function online__preview()
{
$key = trim(Request::input('key'));
//
$data = parse_url($key);
$path = Arr::get($data, 'path');
$file = public_path($path);
//
if (file_exists($file)) {
parse_str($data['query'], $query);
$name = Arr::get($query, 'name');
$ext = strtolower(Arr::get($query, 'ext'));
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
if ($ext === 'pdf'
&& (str_contains($userAgent, 'electron') || str_contains($userAgent, 'chrome'))) {
return Response::download($file, $name, [
'Content-Type' => 'application/pdf'
], 'inline');
}
//
if (in_array($ext, File::localExt)) {
$url = Base::fillUrl($path);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
}
if ($ext !== 'pdf') {
$url = Base::urlAddparameter($url, [
'fullfilename' => $name . '.' . $ext
]);
}
$toUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
return Redirect::to($toUrl, 301);
}
return abort(404);
}
/**
* 设置语言和皮肤
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function setting__theme_language()
{
return view('setting', [
'theme' => Request::input('theme'),
'language' => Request::input('language')
]);
}
/**
* 提取所有中文
* @return array|string
@ -216,9 +418,6 @@ class IndexController extends InvokeController
$list = array_merge(Base::readDir(app_path()), Base::readDir(resource_path()));
$array = [];
foreach ($list as $item) {
if (Base::rightExists($item, "language.all.js")) {
continue;
}
if (Base::rightExists($item, ".php") || Base::rightExists($item, ".vue") || Base::rightExists($item, ".js")) {
$content = file_get_contents($item);
preg_match_all("/(['\"])(.*?)[\u{4e00}-\u{9fa5}\u{FE30}-\u{FFA0}]+([\s\S]((?!\n).)*)\\1/u", $content, $matchs);

View File

@ -24,6 +24,9 @@ class VerifyCsrfToken extends Middleware
// 保存创建项目列表模板
'api/system/column/template/',
// License 设置
'api/system/license/',
// 添加任务
'api/project/task/add/',
@ -36,9 +39,15 @@ class VerifyCsrfToken extends Middleware
// 聊天发文本
'api/dialog/msg/sendtext/',
// 聊天发语音
'api/dialog/msg/sendrecord/',
// 聊天发文件
'api/dialog/msg/sendfile/',
// 聊天发匿名消息
'api/dialog/msg/sendanon/',
// 保存文件内容
'api/file/content/save/',
@ -51,6 +60,12 @@ class VerifyCsrfToken extends Middleware
// 保存汇报
'api/report/store/',
// 签到设置
'api/users/checkin/save/',
// 签到上报
'api/public/checkin/report/',
// 发布桌面端
'desktop/publish/',
];

View File

@ -4,6 +4,7 @@ namespace App\Http\Middleware;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Module\Doo;
use Closure;
use Request;
@ -31,6 +32,7 @@ class WebApi
if (in_array(strtolower($APP_SCHEME), ['https', 'on', 'ssl', '1', 'true', 'yes'], true)) {
$request->setTrustedProxies([$request->getClientIp()], $request::HEADER_X_FORWARDED_PROTO);
}
Doo::load();
return $next($request);
}

249
app/Ldap/LdapUser.php Normal file
View File

@ -0,0 +1,249 @@
<?php
namespace App\Ldap;
use App\Models\User;
use App\Module\Base;
use LdapRecord\Configuration\ConfigurationException;
use LdapRecord\Container;
use LdapRecord\LdapRecordException;
use LdapRecord\Models\Model;
class LdapUser extends Model
{
protected static $init = null;
/**
* The object classes of the LDAP model.
*
* @var array
*/
public static $objectClasses = [
'inetOrgPerson',
'organizationalPerson',
'person',
'top',
'posixAccount',
];
/**
* @return mixed|null
*/
public function getPhoto()
{
return $this->jpegPhoto && is_array($this->jpegPhoto) ? $this->jpegPhoto[0] : null;
}
/**
* @return mixed|null
*/
public function getDisplayName()
{
$nickname = $this->displayName ?: $this->uid;
return is_array($nickname) ? $nickname[0] : $nickname;
}
/**
* @return LdapUser
*/
public static function static(): LdapUser
{
return new static;
}
/**
* 服务是否打开
* @return bool
*/
public static function isOpen(): bool
{
return Base::settingFind('thirdAccessSetting', 'ldap_open') === 'open';
}
/**
* 同步本地是否打开
* @return bool
*/
public static function isSyncLocal(): bool
{
return Base::settingFind('thirdAccessSetting', 'ldap_sync_local') === 'open';
}
/**
* 初始化配置
* @return bool
*/
public static function initConfig()
{
if (is_bool(self::$init)) {
return self::$init;
}
//
$setting = Base::setting('thirdAccessSetting');
if ($setting['ldap_open'] !== 'open') {
return self::$init = false;
}
//
$connection = Container::getDefaultConnection();
try {
$connection->setConfiguration([
"hosts" => [$setting['ldap_host']],
"port" => intval($setting['ldap_port']),
"base_dn" => $setting['ldap_base_dn'],
"username" => $setting['ldap_user_dn'],
"password" => $setting['ldap_password'],
]);
return self::$init = true;
} catch (ConfigurationException $e) {
info($e->getMessage());
return self::$init = false;
}
}
/**
* 获取
* @param $username
* @param $password
* @return Model|null
*/
public static function userFirst($username, $password): ?Model
{
if (!self::initConfig()) {
return null;
}
try {
return self::static()
->where([
'cn' => $username,
'userPassword' => $password
])->first();
} catch (\Exception) {
return null;
}
}
/**
* 登录
* @param $username
* @param $password
* @param User|null $user
* @return User|mixed|null
*/
public static function userLogin($username, $password, $user = null)
{
if (!self::initConfig()) {
return null;
}
$row = self::userFirst($username, $password);
if (!$row) {
return null;
}
if (empty($user)) {
$user = User::reg($username, $password);
}
if ($user) {
$userimg = $row->getPhoto();
if ($userimg) {
$path = "uploads/user/ldap/";
Base::makeDir(public_path($path));
file_put_contents(public_path("{$path}{$user->userid}.jpeg"), $userimg);
$user->userimg = "{$path}{$user->userid}.jpeg";
}
$user->nickname = $row->getDisplayName();
$user->save();
}
return $user;
}
/**
* 同步
* @param User $user
* @param $password
* @return void
*/
public static function userSync(User $user, $password)
{
if ($user->isLdap()) {
return;
}
//
if (!self::initConfig()) {
return;
}
//
if (self::isSyncLocal()) {
$row = self::userFirst($user->email, $password);
if ($row) {
return;
}
try {
$userimg = public_path($user->getRawOriginal('userimg'));
if (file_exists($userimg)) {
$userimg = file_get_contents($userimg);
} else {
$userimg = '';
}
self::static()->create([
'cn' => $user->email,
'gidNumber' => 0,
'homeDirectory' => '/home/ldap/dootask/' . env("APP_NAME"),
'sn' => $user->email,
'uid' => $user->email,
'uidNumber' => $user->userid,
'userPassword' => $password,
'displayName' => $user->nickname,
'jpegPhoto' => $userimg,
]);
$user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['ldap']), ['ldap']));
$user->save();
} catch (LdapRecordException $e) {
info("[LDAP] sync fail: " . $e->getMessage());
}
}
}
/**
* 更新
* @param $username
* @param $array
* @return void
*/
public static function userUpdate($username, $array)
{
if (empty($array)) {
return;
}
if (!self::initConfig()) {
return;
}
try {
$row = self::static()
->where([
'cn' => $username,
])->first();
$row?->update($array);
} catch (\Exception $e) {
info("[LDAP] update fail: " . $e->getMessage());
}
}
/**
* 删除
* @param $username
* @return void
*/
public static function userDelete($username)
{
if (!self::initConfig()) {
return;
}
try {
$row = self::static()
->where([
'cn' => $username,
])->first();
$row?->delete();
} catch (\Exception $e) {
info("[LDAP] delete fail: " . $e->getMessage());
}
}
}

View File

@ -21,7 +21,10 @@ use Illuminate\Support\Facades\DB;
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|static with($relations)
* @method static \Illuminate\Database\Query\Builder|static select($columns = [])
* @method static \Illuminate\Database\Query\Builder|static whereIn($column, $values, $boolean = 'and', $not = false)
* @method static \Illuminate\Database\Query\Builder|static whereNotIn($column, $values, $boolean = 'and')
* @method int change(array $array)
* @method int remove()
* @mixin \Eloquent
*/
class AbstractModel extends Model
@ -38,6 +41,42 @@ class AbstractModel extends Model
protected $appendattrs = [];
/**
* 通过模型修改数据
* @param AbstractModel $builder
* @param $array
* @return int
*/
protected function scopeChange($builder, $array)
{
$line = 0;
$rows = $builder->get();
foreach ($rows as $row) {
$row->updateInstance($array);
if ($row->save()) {
$line++;
}
}
return $line;
}
/**
* 通过模型删除数据
* @param AbstractModel $builder
* @return int
*/
protected function scopeRemove($builder)
{
$line = 0;
$rows = $builder->get();
foreach ($rows as $row) {
if ($row->delete()) {
$line++;
}
}
return $line;
}
/**
* 保存数据忽略错误
* @return bool
@ -46,7 +85,7 @@ class AbstractModel extends Model
{
try {
return $this->save();
} catch (\Exception $e) {
} catch (\Throwable) {
return false;
}
}

88
app/Models/Deleted.php Normal file
View File

@ -0,0 +1,88 @@
<?php
namespace App\Models;
use Carbon\Carbon;
/**
* App\Models\Deleted
*
* @property int $id
* @property string|null $type 删除的数据类型project、task、dialog
* @property int|null $did 删除的数据ID
* @property int|null $userid 关系会员ID
* @property \Illuminate\Support\Carbon|null $created_at
* @method static \Illuminate\Database\Eloquent\Builder|Deleted newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Deleted newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Deleted query()
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereDid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereUserid($value)
* @mixin \Eloquent
*/
class Deleted extends AbstractModel
{
const UPDATED_AT = null;
/**
* 获取删除的ID
* @param $type
* @param $userid
* @param $time
* @return array
*/
public static function ids($type, $userid, $time): array
{
if (empty($time)) {
return [];
}
return self::where([
'type' => $type,
'userid' => $userid
])->where('created_at', '>=', Carbon::parse($time))->pluck('did')->toArray();
}
/**
* 忘记(恢复或添加数据时删除记录)
* @param $type
* @param $id
* @param $userid
* @return void
*/
public static function forget($type, $id, $userid): void
{
if (is_array($userid)) {
self::where([
'type' => $type,
'did' => $id,
])->whereIn('userid', $userid)->delete();
} else {
self::where([
'type' => $type,
'did' => $id,
'userid' => $userid,
])->delete();
}
}
/**
* 记录(删除数据时添加记录)
* @param $type
* @param $id
* @param $userid
* @return void
*/
public static function record($type, $id, $userid): void
{
$array = is_array($userid) ? $userid : [$userid];
foreach ($array as $value) {
self::updateInsert([
'type' => $type,
'did' => $id,
'userid' => $value,
]);
}
}
}

View File

@ -13,8 +13,8 @@ use Request;
* App\Models\File
*
* @property int $id
* @property string|null $pids 上级ID递归
* @property int|null $pid 上级ID
* @property string|null $pids 上级ID递归
* @property int|null $cid 复制ID
* @property string|null $name 名称
* @property string|null $type 类型
@ -22,6 +22,7 @@ use Request;
* @property int|null $size 大小(B)
* @property int|null $userid 拥有者ID
* @property int|null $share 是否共享
* @property int|null $pshare 所属分享ID
* @property int|null $created_id 创建者
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
@ -39,6 +40,7 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|File whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|File wherePid($value)
* @method static \Illuminate\Database\Eloquent\Builder|File wherePids($value)
* @method static \Illuminate\Database\Eloquent\Builder|File wherePshare($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereShare($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereSize($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereType($value)
@ -63,7 +65,7 @@ class File extends AbstractModel
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx'
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx', 'plist'
];
/**
@ -94,21 +96,18 @@ class File extends AbstractModel
/**
* 是否有访问权限
* @param $userid
* @param array $userids
* @return int -1:没有权限0:访问权限1:读写权限1000:所有者或创建者
*/
public function getPermission($userid)
public function getPermission(array $userids)
{
if ($userid == $this->userid || $userid == $this->created_id) {
if (in_array($this->userid, $userids) || in_array($this->created_id, $userids)) {
// ① 自己的文件夹 或 自己创建的文件夹
return 1000;
}
$row = $this->getShareInfo();
if ($row) {
$fileUser = FileUser::whereFileId($row->id)->where(function ($query) use ($userid) {
$query->where('userid', 0);
$query->orWhere('userid', $userid);
})->orderByDesc('permission')->first();
$fileUser = FileUser::whereFileId($row->id)->whereIn('userid', $userids)->orderByDesc('permission')->first();
if ($fileUser) {
// ② 在指定共享成员内
return $fileUser->permission;
@ -142,7 +141,7 @@ class File extends AbstractModel
/**
* 是否处于共享文件夹内(不含自身)
* @return bool
* @return File|false
*/
public function isNnShare()
{
@ -153,13 +152,22 @@ class File extends AbstractModel
break;
}
if ($row->share) {
return true;
return $row;
}
$pid = $row->pid;
}
return false;
}
/**
* 目录内是否存在共享文件或文件夹
* @return bool
*/
public function isSubShare()
{
return $this->type == 'folder' && File::where("pids", "like", "%,{$this->id},%")->whereShare(1)->exists();
}
/**
* 设置/关闭 共享(同时遍历取消里面的共享)
* @param $share
@ -174,6 +182,7 @@ class File extends AbstractModel
AbstractModel::transaction(function () use ($share) {
$this->share = $share;
$this->save();
File::where("pids", "like", "%,{$this->id},%")->update(['pshare' => $share ? $this->id : 0]);
if ($share === 0) {
FileUser::deleteFileAll($this->id, $this->userid);
}
@ -189,16 +198,50 @@ class File extends AbstractModel
}
/**
* 保存前更新pids
* 处理重名
* @return void
*/
public function handleDuplicateName()
{
$builder = self::wherePid($this->pid)->whereUserid($this->userid)->whereExt($this->ext);
$exist = $builder->clone()->whereName($this->name)->exists();
if (!$exist) {
return; // 未重名,不需要处理
}
// 发现重名,自动重命名
$nextNum = 2;
if (preg_match("/(.*?)(\s+\(\d+\))*$/", $this->name)) {
$preName = preg_replace("/(.*?)(\s+\(\d+\))*$/", "$1", $this->name);
$nextNum = $builder->clone()->where("name", "LIKE", "{$preName}%")->count() + 1;
}
$newName = "{$this->name} ({$nextNum})";
if ($builder->clone()->whereName($newName)->exists()) {
$nextNum = rand(100, 9999);
$newName = "{$this->name} ({$nextNum})";
}
$this->name = $newName;
}
/**
* 保存前更新pids/pshare
* @return bool
*/
public function saveBeforePids()
public function saveBeforePP()
{
$pid = $this->pid;
$pshare = $this->share ? $this->id : 0;
$array = [];
while ($pid > 0) {
$array[] = $pid;
$pid = intval(self::whereId($pid)->value('pid'));
$file = self::select(['id', 'pid', 'share'])->find($pid);
if ($file) {
$pid = $file->pid;
if ($file->share) {
$pshare = $file->id;
}
} else {
$pid = 0;
}
}
$opids = $this->pids;
if ($array) {
@ -207,6 +250,7 @@ class File extends AbstractModel
} else {
$this->pids = '';
}
$this->pshare = $pshare;
if (!$this->save()) {
return false;
}
@ -215,7 +259,7 @@ class File extends AbstractModel
self::wherePid($this->id)->chunkById(100, function ($lists) {
/** @var self $item */
foreach ($lists as $item) {
$item->saveBeforePids();
$item->saveBeforePP();
}
});
}
@ -243,6 +287,29 @@ class File extends AbstractModel
return true;
}
/**
* 获取文件分享链接
* @param $userid
* @param $refresh
* @return array
*/
public function getShareLink($userid, $refresh = false)
{
if ($this->type == 'folder') {
throw new ApiException('文件夹不支持分享');
}
return FileLink::generateLink($this->id, $userid, $refresh);
}
/**
* 获取文件名称加后缀
* @return string|null
*/
public function getNameAndExt()
{
return $this->ext ? "{$this->name}.{$this->ext}" : $this->name;
}
/**
* 推送消息
* @param $action
@ -257,19 +324,7 @@ class File extends AbstractModel
];
}
//
if ($userid === null) {
$userid = [$this->userid];
if ($this->share == 1) {
$builder = WebSocket::select(['userid']);
if ($action == 'content') {
$builder->wherePath('file/content/' . $this->id);
}
$userid = array_merge($userid, $builder->pluck('userid')->toArray());
} elseif ($this->share == 2) {
$userid = array_merge($userid, FileUser::whereFileId($this->id)->pluck('userid')->toArray());
}
$userid = array_values(array_filter(array_unique($userid)));
}
$userid = $this->pushUserid($action, $userid);
if (empty($userid)) {
return;
}
@ -293,35 +348,102 @@ class File extends AbstractModel
}
/**
* 处理返回图片地址
* @param $item
* @return void
* 获取推送会员
* @param $action
* @param $userid
* @return array|int[]|mixed|null[]
*/
public static function handleImageUrl(&$item)
public function pushUserid($action, $userid = null) {
$wherePath = "/manage/file";
if ($userid === null) {
$array = [$this->userid];
if ($action == 'add' && $this->pid == 0) {
return $array;
}
if ($action == 'content') {
$wherePath = "/single/file/{$this->id}";
} elseif ($this->pid > 0) {
$wherePath = "/manage/file/{$this->pid}";
} else {
$tmpArray = FileUser::whereFileId($this->id)->pluck('userid')->toArray();
if (empty($tmpArray)) {
return $array;
}
if (!in_array(0, $tmpArray)) {
return $tmpArray;
}
}
$tmpArray = WebSocket::wherePath($wherePath)->pluck('userid')->toArray();
if (empty($tmpArray)) {
return $array;
}
$array = array_values(array_filter(array_unique(array_merge($array, $tmpArray))));
} else {
$array = is_array($userid) ? $userid : [$userid];
if (in_array(0, $array)) {
return WebSocket::wherePath($wherePath)->pluck('userid')->toArray();
}
}
return $array;
}
/**
* code获取文件ID、名称
* @param $code
* @return File
*/
public static function code2IdName($code) {
$arr = explode(",", base64_decode($code));
if (empty($arr)) {
return null;
}
$fileId = intval($arr[0]);
if (empty($fileId)) {
return null;
}
return File::select(['id', 'name'])->find($fileId);
}
/**
* 处理返回图片地址
* @param array $item
* @return array
*/
public static function handleImageUrl($item)
{
if (in_array($item['ext'], self::imageExt) ) {
$content = Base::json2array(FileContent::whereFid($item['id'])->orderByDesc('id')->value('content'));
if ($content) {
$item['image_url'] = Base::fillUrl($content['url']);
$item['image_width'] = intval($content['width']);
$item['image_height'] = intval($content['height']);
}
}
return $item;
}
/**
* 获取文件并检测权限
* @param $id
* @param int $limit 要求权限: 0-访问权限、1-读写权限、1000-所有者或创建者
* @param $permission
* @param int $id
* @param User|array|int $user 要求权限的用户,如:[0, 1]
* @param int $limit 要求权限: 0-访问权限、1-读写权限、1000-所有者或创建者
* @param int $permission
* @return File
*/
public static function permissionFind($id, $limit = 0, &$permission = -1)
public static function permissionFind(int $id, $user, int $limit = 0, int &$permission = -1)
{
$file = File::find($id);
if (empty($file)) {
throw new ApiException('文件不存在或已被删除');
}
//
$permission = $file->getPermission(User::userid());
if ($user instanceof User) {
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
} else {
$userids = is_array($user) ? $user : [$user];
}
$permission = $file->getPermission($userids);
if ($permission < $limit) {
$msg = match ($limit) {
1000 => '仅限所有者或创建者操作',
@ -340,11 +462,10 @@ class File extends AbstractModel
*/
public static function formatFileData(array $data)
{
$fileName = $data['name'];
$filePath = $data['path'];
$fileSize = $data['size'];
$fileExt = $data['ext'];
$fileDotExt = '.' . $fileExt;
$fileName = Base::rightDelete($data['name'], $fileDotExt) . $fileDotExt;
$publicPath = public_path($filePath);
//
switch ($fileExt) {
@ -376,27 +497,27 @@ class File extends AbstractModel
if (in_array($fileExt, self::codeExt) && $fileSize < 2 * 1024 * 1024)
{
// 文本预览限制2M内的文件
$data['content'] = file_get_contents($publicPath) ?: 'Content deleted';
$data['content'] = [
'content' => file_get_contents($publicPath) ?: 'Content deleted',
];
$data['file_mode'] = 'code';
}
elseif (in_array($fileExt, File::officeExt))
{
// office预览
$data['content'] = '';
$data['content'] = json_decode('{}');
$data['file_mode'] = 'office';
}
else
{
// 其他预览
if (in_array($fileExt, File::localExt)) {
$url = Base::fillUrl($filePath);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $filePath;
}
$name = Base::rightDelete($fileName, ".{$fileExt}") . ".{$fileExt}";
$data['content'] = [
'preview' => true,
'url' => base64_encode(Base::urlAddparameter($url, [
'fullfilename' => $fileName
'name' => $name,
'key' => urlencode(Base::urlAddparameter($filePath, [
'name' => $name,
'ext' => $fileExt
])),
];
$data['file_mode'] = 'preview';
@ -405,4 +526,60 @@ class File extends AbstractModel
}
return $data;
}
/**
* 移交文件
* @param $originalUserid
* @param $newUserid
* @return void
*/
public static function transfer($originalUserid, $newUserid)
{
if (!self::whereUserid($originalUserid)->exists()) {
return;
}
// 创建一个文件夹存放移交的文件
$name = User::userid2nickname($originalUserid) ?: ('ID:' . $originalUserid);
$file = File::createInstance([
'pid' => 0,
'name' => "{$name}】移交的文件",
'type' => "folder",
'ext' => "",
'userid' => $newUserid,
'created_id' => 0,
]);
$file->handleDuplicateName();
$file->saveBeforePP();
// 移交文件
self::whereUserid($originalUserid)->chunkById(100, function($list) use ($file, $newUserid) {
/** @var self $item */
foreach ($list as $item) {
if ($item->pid === 0) {
$item->pid = $file->id;
}
$item->userid = $newUserid;
$item->saveBeforePP();
}
});
// 移交文件权限
FileUser::whereUserid($originalUserid)->chunkById(100, function ($list) use ($newUserid) {
/** @var FileUser $item */
foreach ($list as $item) {
$row = FileUser::whereFileId($item->file_id)->whereUserid($newUserid)->first();
if ($row) {
// 已存在则删除原数据,判断改变已存在的数据
$row->permission = max($row->permission, $item->permission);
$row->save();
$item->delete();
} else {
// 不存在则改变原数据
$item->userid = $newUserid;
$item->save();
}
}
});
}
}

View File

@ -5,7 +5,6 @@ namespace App\Models;
use App\Module\Base;
use Illuminate\Database\Eloquent\SoftDeletes;
use Response;
/**
* App\Models\FileContent
@ -40,12 +39,52 @@ class FileContent extends AbstractModel
{
use SoftDeletes;
/**
* 转预览地址
* @param array $array
* @return string
*/
public static function toPreviewUrl($array)
{
$fileExt = $array['ext'];
$fileName = $array['name'];
$filePath = $array['path'];
$name = Base::rightDelete($fileName, ".{$fileExt}") . ".{$fileExt}";
$key = urlencode(Base::urlAddparameter($filePath, [
'name' => $name,
'ext' => $fileExt
]));
return Base::fillUrl("online/preview/{$name}?key={$key}");
}
/**
* 转预览地址
* @param File $file
* @param $content
* @return string
*/
public static function formatPreview($file, $content)
{
$content = Base::json2array($content ?: []);
$filePath = $content['url'];
if (in_array($file->type, ['word', 'excel', 'ppt'])) {
if (empty($content)) {
$filePath = 'assets/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $file->type);
}
}
return self::toPreviewUrl([
'ext' => $file->ext,
'name' => $file->name,
'path' => $filePath,
]);
}
/**
* 获取格式内容(或下载)
* @param File $file
* @param $content
* @param $download
* @return array|\Symfony\Component\HttpFoundation\BinaryFileResponse
* @return array|\Symfony\Component\HttpFoundation\StreamedResponse
*/
public static function formatContent($file, $content, $download = false)
{
@ -53,9 +92,13 @@ class FileContent extends AbstractModel
$content = Base::json2array($content ?: []);
if (in_array($file->type, ['word', 'excel', 'ppt'])) {
if (empty($content)) {
return Response::download(resource_path('assets/statics/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $file->type)), $name);
$filePath = public_path('assets/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $file->type));
} else {
$filePath = public_path($content['url']);
}
return Response::download(public_path($content['url']), $name);
return Base::streamDownload(function() use ($filePath) {
echo file_get_contents($filePath);
}, $name);
}
if (empty($content)) {
$content = match ($file->type) {
@ -84,7 +127,9 @@ class FileContent extends AbstractModel
if ($download) {
$filePath = public_path($path);
if (isset($filePath)) {
return Response::download($filePath, $name);
return Base::streamDownload(function() use ($filePath) {
echo file_get_contents($filePath);
}, $name);
} else {
abort(403, "This file not support download.");
}

View File

@ -2,6 +2,8 @@
namespace App\Models;
use App\Module\Base;
/**
* App\Models\FileLink
*
@ -34,4 +36,35 @@ class FileLink extends AbstractModel
{
return $this->hasOne(File::class, 'id', 'file_id');
}
/**
* 生成链接
* @param $fileId
* @param $userid
* @param $refresh
* @return array
*/
public static function generateLink($fileId, $userid, $refresh = false)
{
$fileLink = FileLink::whereFileId($fileId)->whereUserid($userid)->first();
if (empty($fileLink)) {
$fileLink = FileLink::createInstance([
'file_id' => $fileId,
'userid' => $userid,
'code' => base64_encode("{$fileId},{$userid}," . Base::generatePassword()),
]);
$fileLink->save();
} else {
if ($refresh == 'yes') {
$fileLink->code = base64_encode("{$fileId},{$userid}," . Base::generatePassword());
$fileLink->save();
}
}
return [
'id' => $fileId,
'url' => Base::fillUrl('single/file/' . $fileLink->code),
'code' => $fileLink->code,
'num' => $fileLink->num
];
}
}

34
app/Models/Meeting.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Models;
/**
* App\Models\Meeting
*
* @property int $id
* @property string|null $meetingid 会议ID不是数字
* @property string|null $name 会议主题
* @property string|null $channel 频道
* @property int|null $userid 创建人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $end_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting query()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereChannel($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereEndAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereMeetingid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereUserid($value)
* @mixin \Eloquent
*/
class Meeting extends AbstractModel
{
}

View File

@ -5,6 +5,7 @@ namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Tasks\PushTask;
use Arr;
use Carbon\Carbon;
use DB;
use Hhxsv5\LaravelS\Swoole\Task\Task;
@ -18,6 +19,8 @@ use Request;
* @property string|null $name 名称
* @property string|null $desc 描述、备注
* @property int|null $userid 创建人
* @property int|null $personal 是否个人项目
* @property string|null $user_simple 成员总数|1,2,3
* @property int|null $dialog_id 聊天会话ID
* @property string|null $archived_at 归档时间
* @property int|null $archived_userid 归档会员
@ -45,7 +48,9 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|Project whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project wherePersonal($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUserSimple($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|Project withTrashed()
* @method static \Illuminate\Database\Query\Builder|Project withoutTrashed()
@ -202,9 +207,11 @@ class Project extends AbstractModel
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->dialog_id,
'userid' => $userid,
], [
'important' => 1
]);
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->delete();
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
}
@ -244,8 +251,8 @@ class Project extends AbstractModel
$this->archived_at = null;
$this->archived_userid = User::userid();
$this->addLog("项目取消归档");
$this->pushMsg('add', $this);
ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->update([
$this->pushMsg('recovery', $this);
ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->change([
'archived_at' => null,
'archived_follow' => 0
]);
@ -255,7 +262,7 @@ class Project extends AbstractModel
$this->archived_userid = User::userid();
$this->addLog("项目归档");
$this->pushMsg('archived');
ProjectTask::whereProjectId($this->id)->whereArchivedAt(null)->update([
ProjectTask::whereProjectId($this->id)->whereArchivedAt(null)->change([
'archived_at' => $archived_at,
'archived_follow' => 1
]);
@ -434,7 +441,7 @@ class Project extends AbstractModel
});
//
foreach ($upTaskList as $id => $value) {
ProjectTask::whereFlowItemId($id)->update([
ProjectTask::whereFlowItemId($id)->change([
'flow_item_name' => $value
]);
}
@ -461,6 +468,93 @@ class Project extends AbstractModel
});
}
/**
* 创建项目
* @param $params
* - name 项目名称
* - desc
* - flow
* - personal
* - columns
* @return array
*/
public static function createProject($params, $userid)
{
$name = trim(Arr::get($params, 'name', ''));
$desc = trim(Arr::get($params, 'desc', ''));
$flow = trim(Arr::get($params, 'flow', 'close'));
$isPersonal = intval(Arr::get($params, 'personal'));
if (mb_strlen($name) < 2) {
return Base::retError('项目名称不可以少于2个字');
} elseif (mb_strlen($name) > 32) {
return Base::retError('项目名称最多只能设置32个字');
}
if (mb_strlen($desc) > 255) {
return Base::retError('项目介绍最多只能设置255个字');
}
// 列表
$columns = explode(",", Arr::get($params, 'columns'));
$insertColumns = [];
$sort = 0;
foreach ($columns AS $column) {
$column = trim($column);
if ($column) {
$insertColumns[] = [
'name' => $column,
'sort' => $sort++,
];
}
}
if (empty($insertColumns)) {
$insertColumns[] = [
'name' => 'Default',
'sort' => 0,
];
}
if (count($insertColumns) > 30) {
return Base::retError('项目列表最多不能超过30个');
}
// 开始创建
$project = Project::createInstance([
'name' => $name,
'desc' => $desc,
'userid' => $userid,
]);
if ($isPersonal) {
if (Project::whereUserid($userid)->wherePersonal(1)->exists()) {
return Base::retError('个人项目已存在,无须重复创建');
}
$project->personal = 1;
}
AbstractModel::transaction(function() use ($flow, $insertColumns, $project) {
$project->save();
ProjectUser::createInstance([
'project_id' => $project->id,
'userid' => $project->userid,
'owner' => 1,
])->save();
foreach ($insertColumns AS $column) {
$column['project_id'] = $project->id;
ProjectColumn::createInstance($column)->save();
}
$dialog = WebSocketDialog::createGroup($project->name, $project->userid, 'project');
if (empty($dialog)) {
throw new ApiException('创建项目聊天室失败');
}
$project->dialog_id = $dialog->id;
$project->save();
//
if ($flow == 'open') {
$project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-14,"name":"已取消","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0}]'));
}
});
//
$data = Project::find($project->id);
$data->addLog("创建项目");
$data->pushMsg('add', $data);
return Base::retSuccess('添加成功', $data);
}
/**
* 获取项目信息(用于判断会员是否存在项目内)
* @param int $project_id

View File

@ -81,7 +81,7 @@ class ProjectFlowItem extends AbstractModel
*/
public function deleteFlowItem()
{
ProjectTask::whereFlowItemId($this->id)->update([
ProjectTask::whereFlowItemId($this->id)->change([
'flow_item_id' => 0,
'flow_item_name' => "",
]);

View File

@ -7,12 +7,9 @@ use App\Module\Base;
use App\Tasks\PushTask;
use Arr;
use Carbon\Carbon;
use Config;
use DB;
use Exception;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
use Mail;
use Request;
/**
@ -39,6 +36,8 @@ use Request;
* @property string|null $p_name 优先级名称
* @property string|null $p_color 优先级颜色
* @property int|null $sort 排序(ASC)
* @property string|null $loop 重复周期
* @property string|null $loop_at 下一次重复时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
@ -81,6 +80,8 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoop($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoopAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask wherePColor($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask wherePLevel($value)
@ -303,6 +304,7 @@ class ProjectTask extends AbstractModel
'project_tasks.*',
'project_task_users.owner'
])
->selectRaw("1 AS assist")
->join('project_task_users', 'project_tasks.id', '=', 'project_task_users.task_id')
->where('project_task_users.userid', $userid);
if ($owner !== null) {
@ -529,7 +531,7 @@ class ProjectTask extends AbstractModel
{
AbstractModel::transaction(function () use ($data, &$updateMarking) {
// 判断版本
Base::checkClientVersion('0.6.0');
Base::checkClientVersion('0.19.0');
// 主任务
$mainTask = $this->parent_id > 0 ? self::find($this->parent_id) : null;
// 工作流
@ -626,7 +628,7 @@ class ProjectTask extends AbstractModel
if ($this->complete_at) {
throw new ApiException('任务已完成');
}
$this->completeTask(Carbon::now());
$this->completeTask(Carbon::now(), isset($newFlowItem) ? $newFlowItem->name : null);
} else {
// 标记未完成
if (!$this->complete_at) {
@ -647,10 +649,13 @@ class ProjectTask extends AbstractModel
'change' => [$this->name, $data['name']]
]);
$this->name = $data['name'];
if ($this->dialog_id) {
WebSocketDialog::updateData(['id' => $this->dialog_id], ['name' => $this->name]);
}
}
// 负责人
if (Arr::exists($data, 'owner')) {
$count = $this->taskUser->where('owner', 1)->count();
$older = $this->taskUser->where('owner', 1)->pluck('userid')->toArray();
$array = [];
$owner = is_array($data['owner']) ? $data['owner'] : [$data['owner']];
if (count($owner) > 10) {
@ -669,20 +674,18 @@ class ProjectTask extends AbstractModel
'owner' => 1,
]);
$array[] = $uid;
if ($this->parent_id) {
break; // 子任务只能是一个负责人
}
}
if ($array) {
if ($count == 0 && count($array) == 1 && $array[0] == User::userid()) {
if (count($older) == 0 && count($array) == 1 && $array[0] == User::userid()) {
$this->addLog("认领{任务}");
} else {
$this->addLog("修改{任务}负责人", ['userid' => $array]);
}
$this->taskPush(array_values(array_diff($array, $older)), 0);
}
$rows = ProjectTaskUser::whereTaskId($this->id)->whereOwner(1)->whereNotIn('userid', $array)->get();
if ($rows->isNotEmpty()) {
$this->addLog("删除{任务}负责人", ['userid' => $rows->implode('userid', ',')]);
$this->addLog("删除{任务}负责人", ['userid' => $rows->pluck('userid')]);
foreach ($rows as $row) {
$row->delete();
}
@ -697,7 +700,7 @@ class ProjectTask extends AbstractModel
$this->start_at = null;
$this->end_at = null;
$times = $data['times'];
list($start, $end) = is_string($times) ? explode(",", $times) : (is_array($times) ? $times : []);
list($start, $end, $desc) = is_string($times) ? explode(",", $times) : (is_array($times) ? $times : []);
if (Base::isDate($start) && Base::isDate($end) && $start != $end) {
$start_at = Carbon::parse($start);
$end_at = Carbon::parse($end);
@ -758,15 +761,38 @@ class ProjectTask extends AbstractModel
});
}
$newStringAt = $this->start_at ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
$this->addLog("修改{任务}时间", [
$newDesc = $desc ? "(备注:{$desc}" : "";
$this->addLog("修改{任务}时间" . $newDesc, [
'change' => [$oldStringAt, $newStringAt]
]);
//修改计划时间需要重置任务邮件提醒日志
ProjectTaskMailLog::whereTaskId($this->id)->delete();
$this->taskPush(null, 3, $newDesc);
}
// 以下顶级任务可修改
// 以下仅顶级任务可修改
if ($this->parent_id === 0) {
// 重复周期
$loopAt = $this->loop_at;
$loopDesc = $this->loopDesc();
if (Arr::exists($data, 'loop')) {
$this->loop = $data['loop'];
if (!$this->refreshLoop()) {
throw new ApiException('重复周期选择错误');
}
} elseif (Arr::exists($data, 'times')) {
// 更新任务时间也要更新重复周期
$this->refreshLoop();
}
$oldLoop = $loopAt ? Carbon::parse($loopAt)->toDateTimeString() : null;
$newLoop = $this->loop_at ? Carbon::parse($this->loop_at)->toDateTimeString() : null;
if ($oldLoop != $newLoop) {
$this->addLog("修改{任务}下个周期", [
'change' => [$oldLoop, $newLoop]
]);
}
if ($loopDesc != $this->loopDesc()) {
$this->addLog("修改{任务}重复周期", [
'change' => [$loopDesc, $this->loopDesc()]
]);
}
// 协助人员
if (Arr::exists($data, 'assist')) {
$array = [];
@ -793,7 +819,7 @@ class ProjectTask extends AbstractModel
}
$rows = ProjectTaskUser::whereTaskId($this->id)->whereOwner(0)->whereNotIn('userid', $array)->get();
if ($rows->isNotEmpty()) {
$this->addLog("删除{任务}协助人员", ['userid' => $rows->implode('userid', ',')]);
$this->addLog("删除{任务}协助人员", ['userid' => $rows->pluck('userid')]);
foreach ($rows as $row) {
$row->delete();
}
@ -860,6 +886,141 @@ class ProjectTask extends AbstractModel
return true;
}
/**
* 刷新重复周期时间
* @param bool $save 是否执行保存
* @return bool
*/
public function refreshLoop($save = false)
{
$success = true;
if ($this->start_at) {
$base = Carbon::parse($this->start_at);
if ($base->lt(Carbon::today())) {
// 如果任务开始时间小于今天则基数时间为今天
$base = Carbon::parse(date("Y-m-d {$base->toTimeString()}"));
}
} else {
// 未设置任务时间时基数时间为今天
$base = Carbon::today();
}
switch ($this->loop) {
case "day":
$this->loop_at = $base->addDay();
break;
case "weekdays":
$this->loop_at = $base->addWeekday();
break;
case "week":
$this->loop_at = $base->addWeek();
break;
case "twoweeks":
$this->loop_at = $base->addWeeks(2);
break;
case "month":
$this->loop_at = $base->addMonth();
break;
case "year":
$this->loop_at = $base->addYear();
break;
case "never":
$this->loop_at = null;
break;
default:
if (Base::isNumber($this->loop)) {
$this->loop_at = $base->addDays($this->loop);
} else {
$this->loop_at = null;
$success = false;
}
break;
}
if ($success && $save) {
$this->save();
}
return $success;
}
/**
* 获取周期描述
* @return string
*/
public function loopDesc() {
$loopDesc = "从不";
switch ($this->loop) {
case "day":
$loopDesc = "每天";
break;
case "weekdays":
$loopDesc = "每个工作日";
break;
case "week":
$loopDesc = "每周";
break;
case "twoweeks":
$loopDesc = "每两周";
break;
case "month":
$loopDesc = "每月";
break;
case "year":
$loopDesc = "每年";
break;
default:
if (Base::isNumber($this->loop)) {
$loopDesc = "{$this->loop}";
}
break;
}
return $loopDesc;
}
/**
* 复制任务
* @return self
*/
public function copyTask()
{
if ($this->parent_id > 0) {
throw new ApiException('子任务禁止复制');
}
return AbstractModel::transaction(function() {
// 复制任务
$task = $this->replicate();
$task->dialog_id = 0;
$task->archived_at = null;
$task->archived_userid = 0;
$task->archived_follow = 0;
$task->complete_at = null;
$task->created_at = Carbon::now();
$task->save();
// 复制任务内容
if ($this->content) {
$tmp = $this->content->replicate();
$tmp->task_id = $task->id;
$tmp->created_at = Carbon::now();
$tmp->save();
}
// 复制任务附件
foreach ($this->taskFile as $taskFile) {
$tmp = $taskFile->replicate();
$tmp->task_id = $task->id;
$tmp->created_at = Carbon::now();
$tmp->save();
}
// 复制任务成员
foreach ($this->taskUser as $taskUser) {
$tmp = $taskUser->replicate();
$tmp->task_id = $task->id;
$tmp->task_pid = $task->id;
$tmp->created_at = Carbon::now();
$tmp->save();
}
//
return $task;
});
}
/**
* 同步项目成员至聊天室
*/
@ -879,9 +1040,11 @@ class ProjectTask extends AbstractModel
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->dialog_id,
'userid' => $userid,
], [
'important' => 1
]);
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->delete();
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
}
@ -929,11 +1092,18 @@ class ProjectTask extends AbstractModel
/**
* 权限版本
* @param int $level 1-负责人2-协助人/负责人3-创建人/协助人/负责人
* @param int $level
* 1:负责人
* 2:协助人/负责人
* 3:创建人/协助人/负责人
* 4:任务群聊成员/3
* @return bool
*/
public function permission($level = 1)
{
if ($level >= 4) {
return $this->permission(3) || $this->existDialogUser();
}
if ($level >= 3 && $this->isCreater()) {
return true;
}
@ -943,6 +1113,15 @@ class ProjectTask extends AbstractModel
return $this->isOwner();
}
/**
* 判断是否在任务对话里
* @return bool
*/
public function existDialogUser()
{
return $this->dialog_id && WebSocketDialogUser::whereDialogId($this->dialog_id)->whereUserid(User::userid())->exists();
}
/**
* 判断是否创建者
* @return bool
@ -998,15 +1177,22 @@ class ProjectTask extends AbstractModel
/**
* 标记已完成、未完成
* @param Carbon|null $complete_at 完成时间
* @param String $complete_name 已完成名称(留空为:已完成)
* @return bool
*/
public function completeTask($complete_at)
public function completeTask($complete_at, $complete_name = null)
{
AbstractModel::transaction(function () use ($complete_at) {
AbstractModel::transaction(function () use ($complete_at, $complete_name) {
$addMsg = $this->parent_id == 0 && $this->dialog_id > 0;
if ($complete_at === null) {
// 标记未完成
$this->complete_at = null;
$this->addLog("标记{任务}未完成");
if ($addMsg) {
WebSocketDialogMsg::sendMsg(null, $this->dialog_id, 'notice', [
'notice' => "标记任务未完成"
], 0, true, true);
}
} else {
// 标记已完成
if ($this->parent_id == 0) {
@ -1017,8 +1203,16 @@ class ProjectTask extends AbstractModel
if (!$this->hasOwner()) {
throw new ApiException('请先领取任务');
}
if (empty($complete_name)) {
$complete_name = '已完成';
}
$this->complete_at = $complete_at;
$this->addLog("标记{任务}已完成");
$this->addLog("标记{任务}{$complete_name}");
if ($addMsg) {
WebSocketDialogMsg::sendMsg(null, $this->dialog_id, 'notice', [
'notice' => "标记任务{$complete_name}"
], 0, true, true);
}
}
$this->save();
});
@ -1063,12 +1257,12 @@ class ProjectTask extends AbstractModel
$this->archived_follow = 0;
$this->addLog($logText, [], $userid);
}
$this->pushMsg('update', [
$this->pushMsg($archived_at === null ? 'recovery' : 'archived', [
'id' => $this->id,
'archived_at' => $this->archived_at,
'archived_userid' => $this->archived_userid,
]);
self::whereParentId($this->id)->update([
self::whereParentId($this->id)->change([
'archived_at' => $this->archived_at,
'archived_userid' => $this->archived_userid,
'archived_follow' => $this->archived_follow,
@ -1090,7 +1284,7 @@ class ProjectTask extends AbstractModel
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog?->deleteDialog();
}
self::whereParentId($this->id)->delete();
self::whereParentId($this->id)->remove();
$this->deleted_userid = User::userid();
$this->save();
$this->addLog("删除{任务}");
@ -1107,12 +1301,12 @@ class ProjectTask extends AbstractModel
* @param bool $pushMsg 是否推送
* @return bool
*/
public function recoveryTask($pushMsg = true)
public function restoreTask($pushMsg = true)
{
AbstractModel::transaction(function () {
if ($this->dialog_id) {
$dialog = WebSocketDialog::withTrashed()->find($this->dialog_id);
$dialog?->recoveryDialog();
$dialog?->restoreDialog();
}
self::whereParentId($this->id)->withTrashed()->restore();
$this->addLog("还原{任务}");
@ -1175,28 +1369,62 @@ class ProjectTask extends AbstractModel
$data = $data->toArray();
}
//
$array = [$userid, []];
$userids = [];
if ($userid === null) {
$array[0] = $this->project->relationUserids();
$userids = $this->project->relationUserids();
} elseif (!is_array($userid)) {
$array[0] = [$userid];
$userids = [$userid];
}
//
if (isset($data['owner'])) {
$owners = ProjectTaskUser::whereTaskId($data['id'])->whereOwner(1)->pluck('userid')->toArray();
$array = [array_intersect($array[0], $owners), array_diff($array[0], $owners)];
}
foreach ($array as $index => $item) {
if ($index > 0) {
$data['owner'] = 0;
$array = [];
if (empty($data['parent_id'])) {
if (Arr::exists($data, 'owner') || Arr::exists($data, 'assist')) {
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->get();
// 负责人
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray();
$owners = array_intersect($userids, $owners);
if ($owners) {
$array[] = [
'userid' => array_values($owners),
'data' => array_merge($data, [
'owner' => 1,
'assist' => 1,
])
];
}
// 协助人
$assists = $taskUser->pluck('userid')->toArray();
$assists = array_intersect($userids, array_diff($assists, $owners));
if ($assists) {
$array[] = [
'userid' => array_values($assists),
'data' => array_merge($data, [
'owner' => 0,
'assist' => 1,
])
];
}
// 项目成员(其他人)
$userids = array_diff($userids, $owners, $assists);
$data = array_merge($data, [
'owner' => 0,
'assist' => 0,
]);
}
}
$array[] = [
'userid' => array_values($userids),
'data' => $data
];
//
foreach ($array as $item) {
$params = [
'ignoreFd' => Request::header('fd'),
'userid' => array_values($item),
'msg' => [
'type' => 'projectTask',
'action' => $action,
'data' => $data,
'data' => $item['data'],
]
];
$task = new PushTask($params, false);
@ -1204,6 +1432,62 @@ class ProjectTask extends AbstractModel
}
}
/**
* 任务提醒
* @param $userids
* @param int $type 0-新任务、1-即将超时、2-已超时、3-修改时间
* @param string $suffix 描述后缀
* @return void
*/
public function taskPush($userids, int $type, string $suffix = "")
{
if ($userids === null) {
$userids = $this->taskUser->pluck('userid')->toArray();
}
if (empty($userids)) {
return;
}
$owners = $this->taskUser->pluck('owner', 'userid')->toArray();
$receivers = User::whereIn('userid', $userids)->whereNull('disable_at')->get();
if (empty($receivers)) {
return;
}
$botUser = User::botGetOrCreate('task-alert');
if (empty($botUser)) {
return;
}
$taskHtml = "<span class=\"mention task\" data-id=\"{$this->id}\">#{$this->name}</span>";
$text = match ($type) {
1 => "您的任务 {$taskHtml} 即将超时。",
2 => "您的任务 {$taskHtml} 已经超时。",
3 => "您的任务 {$taskHtml} 时间已修改。",
default => "您有一个新任务 {$taskHtml}",
};
/** @var User $user */
foreach ($receivers as $receiver) {
$data = [
'type' => $type,
'userid' => $receiver->userid,
'task_id' => $this->id,
];
if (in_array($type, [1, 2]) && ProjectTaskPushLog::where($data)->exists()) {
continue;
}
//
$replace = $owners[$receiver->userid] ? "您负责的任务" : "您协助的任务";
$dialog = WebSocketDialog::checkUserDialog($botUser, $receiver->userid);
if ($dialog) {
ProjectTaskPushLog::createInstance($data)->save();
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', [
'text' => str_replace("您的任务", $replace, $text) . $suffix
], $botUser->userid);
}
}
}
/**
* 获取任务
* @param $task_id
@ -1211,7 +1495,15 @@ class ProjectTask extends AbstractModel
*/
public static function oneTask($task_id)
{
return self::with(['taskUser', 'taskTag'])->allData()->where("project_tasks.id", intval($task_id))->first();
$data = self::with(['taskUser', 'taskTag'])->allData()->where("project_tasks.id", intval($task_id))->first();
if ($data && $data->parent_id === 0) {
if ($data->owner || ProjectTaskUser::select(['owner'])->whereTaskId($data->id)->whereUserid(User::userid())->exists()) {
$data->assist = 1;
} else {
$data->assist = 0;
}
}
return $data;
}
/**
@ -1219,7 +1511,10 @@ class ProjectTask extends AbstractModel
* @param int $task_id
* @param bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param bool $trashed true:仅限未删除, false:仅限已删除, null:不限制
* @param int|bool $permission 0|false:不限制, 1|true:限制项目负责人、任务负责人、协助人员及任务创建者, 2:已有负责人才限制true (子任务时如果是主任务负责人也可以)
* @param int|bool $permission
* - 0|false 限制:项目成员、任务成员、任务群聊成员(任务成员 = 任务创建人+任务协助人+任务负责人)
* - 1|true 限制:项目负责人、任务成员
* - 2 已有负责人才限制true (子任务时如果是主任务负责人也可以)
* @param array $with
* @return self
*/
@ -1245,20 +1540,21 @@ class ProjectTask extends AbstractModel
//
try {
$project = Project::userProject($task->project_id);
} catch (Exception $e) {
if ($task->owner === null) {
} catch (\Throwable $e) {
if ($task->owner !== null || (!$permission && $task->permission(4))) {
$project = Project::find($task->project_id);
if (empty($project)) {
throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002);
}
} else {
throw new ApiException($e->getMessage(), [ 'task_id' => $task_id ], -4002);
}
$project = Project::find($task->project_id);
if (empty($project)) {
throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002);
}
}
//
if ($permission === 2) {
if ($permission >= 2) {
$permission = $task->hasOwner() ? 1 : 0;
}
if (($permission === 1 || $permission === true) && !$project->owner && !$task->permission(3)) {
if ($permission && !$project->owner && !$task->permission(3)) {
throw new ApiException('仅限项目负责人、任务负责人、协助人员或任务创建者操作');
}
//

View File

@ -61,13 +61,14 @@ class ProjectTaskContent extends AbstractModel
{
$path = 'uploads/task/content/' . date("Ym") . '/' . $task_id . '/';
//
preg_match_all("/<img\s*src=\"data:image\/(png|jpg|jpeg);base64,(.*?)\"/s", $content, $matchs);
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg);base64,(.*?)\"/s", $content, $matchs);
foreach ($matchs[2] as $key => $text) {
$tmpPath = $path . 'attached/';
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5($text) . "." . $matchs[1][$key];
if (file_put_contents(public_path($tmpPath), base64_decode($text))) {
$content = str_replace($matchs[0][$key], '<img src="{{RemoteURL}}' . $tmpPath . '"', $content);
$paramet = getimagesize(public_path($tmpPath));
$content = str_replace($matchs[0][$key], '<img src="{{RemoteURL}}' . $tmpPath . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
}
}
$pattern = '/<img(.*?)src=("|\')https*:\/\/(.*?)\/(uploads\/task\/content\/(.*?))\2/is';

View File

@ -3,6 +3,7 @@
namespace App\Models;
use App\Module\Base;
use Cache;
/**
* App\Models\ProjectTaskFile
@ -19,6 +20,8 @@ use App\Module\Base;
* @property int|null $download 下载次数
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read int $height
* @property-read int $width
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile query()
@ -38,6 +41,11 @@ use App\Module\Base;
*/
class ProjectTaskFile extends AbstractModel
{
protected $appends = [
'width',
'height',
];
/**
* 地址
* @param $value
@ -57,4 +65,50 @@ class ProjectTaskFile extends AbstractModel
{
return Base::fillUrl($value ?: Base::extIcon($this->ext));
}
/**
*
* @return int
*/
public function getWidthAttribute()
{
$this->generateSizeData();
return $this->appendattrs['width'];
}
/**
*
* @return int
*/
public function getHeightAttribute()
{
$this->generateSizeData();
return $this->appendattrs['height'];
}
/**
* 生成尺寸数据
*/
private function generateSizeData()
{
if (!isset($this->appendattrs['width'])) {
$width = -1;
$height = -1;
if (in_array($this->ext, ['jpg', 'jpeg', 'gif', 'png'])) {
$path = public_path($this->getRawOriginal('path'));
[$width, $height] = Cache::remember("File::size-" . md5($path), now()->addDays(7), function () use ($path) {
$width = -1;
$height = -1;
if (file_exists($path)) {
$paramet = getimagesize($path);
$width = $paramet[0];
$height = $paramet[1];
}
return [$width, $height];
});
}
$this->appendattrs['width'] = $width;
$this->appendattrs['height'] = $height;
}
}
}

View File

@ -8,37 +8,31 @@ namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* App\Models\ProjectTaskMailLog
* App\Models\ProjectTaskPushLog
*
* @property int $id
* @property int|null $userid 用户id
* @property int|null $task_id 任务id
* @property string|null $email 电子邮箱
* @property int|null $type 提醒类型1第一次任务提醒2第二次任务超期提醒
* @property int|null $is_send 邮件发送是否成功0否1是
* @property string|null $send_error 邮件发送错误详情
* @property int|null $type 提醒类型0 任务开始提醒1 距离到期提醒2到期超时提醒
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog newQuery()
* @method static \Illuminate\Database\Query\Builder|ProjectTaskMailLog onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereIsSend($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereSendError($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|ProjectTaskMailLog withTrashed()
* @method static \Illuminate\Database\Query\Builder|ProjectTaskMailLog withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog newQuery()
* @method static \Illuminate\Database\Query\Builder|ProjectTaskPushLog onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|ProjectTaskPushLog withTrashed()
* @method static \Illuminate\Database\Query\Builder|ProjectTaskPushLog withoutTrashed()
* @mixin \Eloquent
*/
class ProjectTaskMailLog extends AbstractModel
class ProjectTaskPushLog extends AbstractModel
{
use SoftDeletes;

View File

@ -13,6 +13,7 @@ namespace App\Models;
* @property int|null $owner 是否任务负责人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\ProjectTask|null $projectTask
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser query()
@ -29,4 +30,45 @@ namespace App\Models;
class ProjectTaskUser extends AbstractModel
{
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function projectTask(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(ProjectTask::class, 'id', 'task_id');
}
/**
* 移交任务身份
* @param $originalUserid
* @param $newUserid
* @return void
*/
public static function transfer($originalUserid, $newUserid)
{
self::whereUserid($originalUserid)->chunk(100, function ($list) use ($originalUserid, $newUserid) {
$tastIds = [];
/** @var self $item */
foreach ($list as $item) {
$row = self::whereTaskId($item->task_id)->whereUserid($newUserid)->first();
if ($row) {
// 已存在则删除原数据,判断改变已存在的数据
$row->owner = max($row->owner, $item->owner);
$row->save();
$item->delete();
} else {
// 不存在则改变原数据
$item->userid = $newUserid;
$item->save();
}
if ($item->projectTask) {
$item->projectTask->addLog("移交{任务}身份", ['userid' => [$originalUserid, ' => ',$newUserid]]);
if (!in_array($item->task_pid, $tastIds)) {
$tastIds[] = $item->task_pid;
$item->projectTask->syncDialogUser();
}
}
}
});
}
}

View File

@ -2,8 +2,6 @@
namespace App\Models;
use App\Module\Base;
/**
* App\Models\ProjectUser
*
@ -38,6 +36,41 @@ class ProjectUser extends AbstractModel
return $this->hasOne(Project::class, 'id', 'project_id');
}
/**
* 移交项目身份
* @param $originalUserid
* @param $newUserid
* @return void
*/
public static function transfer($originalUserid, $newUserid)
{
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
/** @var self $item */
foreach ($list as $item) {
$row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first();
if ($row) {
// 已存在则删除原数据,判断改变已存在的数据
$row->owner = max($row->owner, $item->owner);
$row->save();
$item->delete();
} else {
// 不存在则改变原数据
$item->userid = $newUserid;
$item->save();
}
if ($item->project) {
if ($item->project->personal) {
$name = User::userid2nickname($originalUserid) ?: ('ID:' . $originalUserid);
$item->project->name = "{$name}{$item->project->name}";
$item->project->save();
}
$item->project->addLog("移交项目身份", ['userid' => [$originalUserid, ' => ', $newUserid]]);
$item->project->syncDialogUser();
}
}
});
}
/**
* 退出项目
*/
@ -47,15 +80,13 @@ class ProjectUser extends AbstractModel
->whereUserid($this->userid)
->chunk(100, function ($list) {
$tastIds = [];
/** @var ProjectTaskUser $item */
foreach ($list as $item) {
$item->delete();
if (!in_array($item->task_pid, $tastIds)) {
$tastIds[] = $item->task_pid;
$item->projectTask?->syncDialogUser();
}
$item->delete();
}
$tasks = ProjectTask::whereIn('id', $tastIds)->get();
foreach ($tasks as $task) {
$task->syncDialogUser();
}
});
$this->delete();

View File

@ -2,6 +2,8 @@
namespace App\Models;
use App\Module\Base;
/**
* App\Models\Setting
*
@ -24,5 +26,40 @@ namespace App\Models;
*/
class Setting extends AbstractModel
{
/**
* 验证邮箱地址(过滤忽略地址)
* @param $array
* @param \Closure $resultClosure
* @param \Closure|null $emptyClosure
* @return array|mixed
*/
public static function validateAddr($array, $resultClosure, $emptyClosure = null)
{
if (!is_array($array)) {
$array = [$array];
}
$ignoreAddr = Base::settingFind('emailSetting', 'ignore_addr');
$ignoreAddr = explode("\n", $ignoreAddr);
$ignoreArray = ['admin@dootask.com', 'test@dootask.com'];
foreach ($ignoreAddr as $item) {
if (Base::isEmail($item)) {
$ignoreArray[] = trim($item);
}
}
if ($ignoreArray) {
$array = array_diff($array, $ignoreArray);
}
if ($array) {
if ($resultClosure instanceof \Closure) {
foreach ($array as $value) {
$resultClosure($value);
}
}
} else {
if ($emptyClosure instanceof \Closure) {
$emptyClosure();
}
}
return $array;
}
}

37
app/Models/TaskWorker.php Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* App\Models\TaskWorker
*
* @property int $id
* @property string|null $args
* @property string|null $error
* @property string|null $start_at 开始时间
* @property string|null $end_at 结束时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker newQuery()
* @method static \Illuminate\Database\Query\Builder|TaskWorker onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker query()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereArgs($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereEndAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereError($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereStartAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereUpdatedAt($value)
* @method static \Illuminate\Database\Query\Builder|TaskWorker withTrashed()
* @method static \Illuminate\Database\Query\Builder|TaskWorker withoutTrashed()
* @mixin \Eloquent
*/
class TaskWorker extends AbstractModel
{
use SoftDeletes;
}

171
app/Models/UmengAlias.php Normal file
View File

@ -0,0 +1,171 @@
<?php
namespace App\Models;
use App\Module\Base;
use Carbon\Carbon;
use Hedeqiang\UMeng\Android;
use Hedeqiang\UMeng\IOS;
/**
* App\Models\UmengAlias
*
* @property int $id
* @property int|null $userid 会员ID
* @property string|null $alias 别名
* @property string|null $platform 平台类型
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias query()
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereAlias($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias wherePlatform($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUserid($value)
* @mixin \Eloquent
*/
class UmengAlias extends AbstractModel
{
protected $table = 'umeng_alias';
/**
* 获取推送配置
* @return array|false
*/
public static function getPushConfig()
{
$setting = Base::setting('appPushSetting');
if ($setting['push'] !== 'open') {
return false;
}
$config = [];
if ($setting['ios_key']) {
$config['iOS'] = [
'appKey' => $setting['ios_key'],
'appMasterSecret' => $setting['ios_secret'],
'production_mode' => true,
];
}
if ($setting['android_key']) {
$config['Android'] = [
'appKey' => $setting['android_key'],
'appMasterSecret' => $setting['android_secret'],
'production_mode' => true,
];
}
return $config;
}
/**
* 推送消息
* @param string $alias
* @param string $platform
* @param array $array [title, subtitle, body, description, extra, seconds, badge]
* @return array|false
*/
public static function pushMsgToAlias($alias, $platform, $array)
{
$config = self::getPushConfig();
if ($config === false) {
return false;
}
//
$title = $array['title'] ?: ''; // 标题
$subtitle = $array['subtitle'] ?: ''; // 副标题iOS
$body = $array['body'] ?: ''; // 通知内容
$description = $array['description'] ?: 'no description'; // 描述
$extra = is_array($array['extra']) ? $array['extra'] : []; // 额外参数
$seconds = intval($array['seconds']) ?: 86400; // 有效时间(单位:秒)
$badge = intval($array['badge']) ?: 0; // 角标数iOS
//
switch ($platform) {
case 'ios':
if (!isset($config['iOS'])) {
return false;
}
$ios = new IOS($config);
return $ios->send([
'description' => $description,
'payload' => array_merge([
'aps' => [
'alert' => [
'title' => $title,
'subtitle' => $subtitle,
'body' => $body,
],
'sound' => 'default',
'badge' => $badge,
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
]);
case 'android':
if (!isset($config['Android'])) {
return false;
}
$android = new Android($config);
return $android->send([
'description' => $description,
'payload' => array_merge([
'display_type' => 'notification',
'body' => [
'ticker' => $title,
'text' => $body,
'title' => $title,
'after_open' => 'go_app',
'play_sound' => true,
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'mipush' => true,
'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity',
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
]
]);
default:
return false;
}
}
/**
* 推送给指定会员
* @param array|int $userid
* @param array $array
* @return void
*/
public static function pushMsgToUserid($userid, $array)
{
$builder = self::select(['id', 'platform', 'alias', 'userid'])->where('updated_at', '>', Carbon::now()->subMonth());
if (is_array($userid)) {
$builder->whereIn('userid', $userid);
} elseif (Base::isNumber($userid)) {
$builder->whereUserid($userid);
}
$builder
->orderByDesc('id')
->chunkById(100, function ($datas) use ($array) {
$uids = $datas->groupBy('userid');
foreach ($uids as $uid => $rows) {
$array['badge'] = WebSocketDialogMsgRead::whereUserid($uid)->whereSilence(0)->whereReadAt(null)->count();
$lists = $rows->groupBy('platform');
foreach ($lists as $platform => $list) {
$alias = $list->pluck('alias')->implode(',');
self::pushMsgToAlias($alias, $platform, $array);
}
}
});
}
}

View File

@ -5,6 +5,7 @@ namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use Cache;
use Carbon\Carbon;
@ -13,8 +14,11 @@ use Carbon\Carbon;
*
* @property int $userid
* @property array $identity 身份
* @property array $department 所属部门
* @property string|null $az A-Z
* @property string|null $pinyin 拼音(主要用于搜索)
* @property string|null $email 邮箱
* @property string|null $tel 联系电话
* @property string $nickname 昵称
* @property string|null $profession 职位/职称
* @property string $userimg 头像
@ -28,8 +32,9 @@ use Carbon\Carbon;
* @property string|null $line_at 最后在线时间(接口)
* @property int|null $task_dialog_id 最后打开的任务会话ID
* @property string|null $created_ip 注册IP
* @property string|null $disable_at 禁用时间
* @property string|null $disable_at 禁用时间(离职时间)
* @property int|null $email_verity 邮箱是否已验证
* @property int|null $bot 是否机器人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Database\Factories\UserFactory factory(...$parameters)
@ -37,9 +42,11 @@ use Carbon\Carbon;
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|User query()
* @method static \Illuminate\Database\Eloquent\Builder|User whereAz($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereBot($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereChangepass($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedIp($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereDepartment($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereDisableAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereEmailVerity($value)
@ -52,8 +59,10 @@ use Carbon\Carbon;
* @method static \Illuminate\Database\Eloquent\Builder|User whereLoginNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereNickname($value)
* @method static \Illuminate\Database\Eloquent\Builder|User wherePassword($value)
* @method static \Illuminate\Database\Eloquent\Builder|User wherePinyin($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereProfession($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereTaskDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereTel($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereUserimg($value)
@ -64,22 +73,14 @@ class User extends AbstractModel
protected $primaryKey = 'userid';
protected $hidden = [
'disable_at',
'updated_at',
];
/**
* 更新数据校验
* @param array $param
*/
public function updateInstance(array $param)
{
parent::updateInstance($param);
//
if (isset($param['line_at']) && $this->userid) {
Cache::put("User::online:" . $this->userid, time(), Carbon::now()->addSeconds(30));
}
}
// 默认头像类型auto自动生成system系统默认
public static $defaultAvatarMode = 'auto';
// 基本信息的字段
public static $basicField = ['userid', 'email', 'nickname', 'profession', 'department', 'userimg', 'bot', 'az', 'pinyin', 'line_at', 'disable_at'];
/**
* 昵称
@ -98,11 +99,7 @@ class User extends AbstractModel
*/
public function getUserimgAttribute($value)
{
if ($value) {
return Base::fillUrl($value);
}
$name = ($this->userid - 1) % 21 + 1;
return url("images/avatar/default_{$name}.png");
return self::getAvatar($this->userid, $value, $this->email, $this->nickname);
}
/**
@ -118,13 +115,47 @@ class User extends AbstractModel
return array_filter(is_array($value) ? $value : explode(",", trim($value, ",")));
}
/**
* 部门
* @param $value
* @return array
*/
public function getDepartmentAttribute($value)
{
if (empty($value)) {
return [];
}
return array_filter(is_array($value) ? $value : Base::explodeInt($value));
}
/**
* 获取所属部门名称
* @return string
*/
public function getDepartmentName()
{
if (empty($this->department)) {
return "";
}
$key = "UserDepartment::" . md5(Cache::get("UserDepartment::rand") . '-' . implode(',' , $this->department));
$list = Cache::remember($key, now()->addMonth(), function() {
$list = UserDepartment::select(['id', 'owner_userid', 'name'])->whereIn('id', $this->department)->take(10)->get();
return $list->toArray();
});
$array = [];
foreach ($list as $item) {
$array[] = $item['name'] . ($item['owner_userid'] === $this->userid ? '(M)' : '');
}
return implode(', ', $array);
}
/**
* 是否在线
* @return bool
*/
public function getOnlineStatus()
{
$online = intval(Cache::get("User::online:" . $this->userid, 0));
$online = $this->bot || Cache::get("User::online:" . $this->userid) === "on";
if ($online) {
return true;
}
@ -132,9 +163,45 @@ class User extends AbstractModel
}
/**
* 判断是否管理员
* 返回是否LDAP用户
* @return bool
*/
public function isLdap()
{
return in_array('ldap', $this->identity);
}
/**
* 返回是否临时帐号
* @return bool
*/
public function isTemp()
{
return in_array('temp', $this->identity);
}
/**
* 返回是否禁用帐号(离职)
* @return bool
*/
public function isDisable()
{
return in_array('disable', $this->identity);
}
/**
* 返回是否管理员
* @return bool
*/
public function isAdmin()
{
return in_array('admin', $this->identity);
}
/**
* 判断是否管理员
*/
public function checkAdmin()
{
$this->identity('admin');
}
@ -169,6 +236,36 @@ class User extends AbstractModel
}
}
/**
* 删除会员
* @param $reason
* @return bool|null
*/
public function deleteUser($reason)
{
return AbstractModel::transaction(function () use ($reason) {
// 删除原因
$userDelete = UserDelete::createInstance([
'operator' => User::userid(),
'userid' => $this->userid,
'email' => $this->email,
'reason' => $reason,
'cache' => array_merge($this->getRawOriginal(), [
'department_name' => $this->getDepartmentName()
])
]);
$userDelete->save();
// 删除未读
WebSocketDialogMsgRead::whereUserid($this->userid)->delete();
// 删除待办
WebSocketDialogMsgTodo::whereUserid($this->userid)->delete();
// 删除邮箱验证记录
UserEmailVerification::whereEmail($this->email)->delete();
//
return $this->delete();
});
}
/** ***************************************************************************************** */
/** ***************************************************************************************** */
/** ***************************************************************************************** */
@ -182,105 +279,47 @@ class User extends AbstractModel
*/
public static function reg($email, $password, $other = [])
{
//邮箱
// 邮箱
if (!Base::isEmail($email)) {
throw new ApiException('请输入正确的邮箱地址');
}
if (User::email2userid($email) > 0) {
$user = self::whereEmail($email)->first();
if ($user) {
$isRegVerify = Base::settingFind('emailSetting', 'reg_verify') === 'open';
$user = self::whereUserid(User::email2userid($email))->first();
if ($isRegVerify && $user->email_verity === 0) {
UserEmailVerification::userEmailSend($user);
throw new ApiException('您的号已注册过,请验证邮箱', ['code' => 'email']);
throw new ApiException('您的号已注册过,请验证邮箱', ['code' => 'email']);
}
throw new ApiException('邮箱地址已存在');
}
//密码
// 密码
self::passwordPolicy($password);
//开始注册
$encrypt = Base::generatePassword(6);
$inArray = [
'encrypt' => $encrypt,
'email' => $email,
'password' => Base::md52($password, $encrypt),
'created_ip' => Base::getIp(),
];
// 开始注册
$user = Doo::userCreate($email, $password);
if ($other) {
$inArray = array_merge($inArray, $other);
$user->updateInstance($other);
}
$user->az = Base::getFirstCharter($user->nickname);
$user->pinyin = Base::cn2pinyin($user->nickname);
$user->created_ip = Base::getIp();
if ($user->save()) {
$setting = Base::setting('system');
$reg_identity = $setting['reg_identity'] ?: 'normal';
$all_group_autoin = $setting['all_group_autoin'] ?: 'yes';
// 注册临时身份
if ($reg_identity === 'temp') {
$user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['temp']), ['temp']));
$user->save();
}
// 加入全员群组
if ($all_group_autoin === 'yes') {
$dialog = WebSocketDialog::whereGroupType('all')->orderByDesc('id')->first();
$dialog?->joinGroup($user->userid, 0);
}
}
$user = User::createInstance($inArray);
$user->save();
User::AZUpdate($user->userid);
return $user->find($user->userid);
}
/**
* 邮箱获取userid
* @param $email
* @return int
*/
public static function email2userid($email)
{
if (empty($email)) {
return 0;
}
return intval(self::whereEmail($email)->value('userid'));
}
/**
* token获取会员userid
* @return int
*/
public static function token2userid()
{
return self::authFind('userid', Base::getToken());
}
/**
* token获取会员邮箱
* @return int
*/
public static function token2email()
{
return self::authFind('email', Base::getToken());
}
/**
* token获取encrypt
* @return mixed|string
*/
public static function token2encrypt()
{
return self::authFind('encrypt', Base::getToken());
}
/**
* 获取token身份信息
* @param $find
* @param null $token
* @return array|mixed|string
*/
public static function authFind($find, $token = null)
{
if ($token === null) {
$token = Base::getToken();
}
list($userid, $email, $encrypt, $timestamp) = explode("#$", base64_decode($token) . "#$#$#$#$");
$array = [
'userid' => intval($userid),
'email' => $email ?: '',
'encrypt' => $encrypt ?: '',
'timestamp' => intval($timestamp),
];
if (isset($array[$find])) {
return $array[$find];
}
if ($find == 'all') {
return $array;
}
return '';
}
/**
* 获取我的ID
* @return int
@ -316,8 +355,7 @@ class User extends AbstractModel
{
$user = self::authInfo();
if (!$user) {
$authorization = Base::getToken();
if ($authorization) {
if (Base::headerOrInput('token')) {
throw new ApiException('身份已失效,请重新登录', [], -1);
} else {
throw new ApiException('请登录后继续...', [], -1);
@ -342,57 +380,49 @@ class User extends AbstractModel
if (isset($_A["__static_auth"])) {
return $_A["__static_auth"];
}
$authorization = Base::getToken();
if ($authorization) {
$authInfo = self::authFind('all', $authorization);
if ($authInfo['userid'] > 0) {
$loginValid = floatval(Base::settingFind('system', 'loginValid')) ?: 720;
$loginValid *= 3600;
if ($authInfo['timestamp'] + $loginValid > time()) {
$row = self::whereUserid($authInfo['userid'])->whereEmail($authInfo['email'])->whereEncrypt($authInfo['encrypt'])->first();
if ($row) {
$upArray = [];
if (Base::getIp() && $row->line_ip != Base::getIp()) {
$upArray['line_ip'] = Base::getIp();
}
if (Carbon::parse($row->line_at)->addSeconds(30)->lt(Carbon::now())) {
$upArray['line_at'] = Carbon::now();
}
if ($upArray) {
$row->updateInstance($upArray);
$row->save();
}
return $_A["__static_auth"] = $row;
}
}
if (Doo::userId() > 0
&& !Doo::userExpired()
&& $user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first()) {
$upArray = [];
if (Base::getIp() && $user->line_ip != Base::getIp()) {
$upArray['line_ip'] = Base::getIp();
}
if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) {
$upArray['line_at'] = Carbon::now();
}
if ($upArray) {
$user->updateInstance($upArray);
$user->save();
}
return $_A["__static_auth"] = $user;
}
return $_A["__static_auth"] = false;
}
/**
* 生成token
* 生成 token
* @param self $userinfo
* @param bool $refresh 获取新的token
* @return string
*/
public static function token($userinfo)
public static function generateToken($userinfo, $refresh = false)
{
$userinfo->token = base64_encode($userinfo->userid . '#$' . $userinfo->email . '#$' . $userinfo->encrypt . '#$' . time() . '#$' . Base::generatePassword(6));
if (!$refresh) {
if (Doo::userId() != $userinfo->userid
|| Doo::userEmail() != $userinfo->email
|| Doo::userEncrypt() != $userinfo->encrypt) {
$refresh = true;
}
}
if ($refresh) {
$days = $userinfo->bot ? 0 : max(1, intval(Base::settingFind('system', 'token_valid_days', 15)));
$token = Doo::tokenEncode($userinfo->userid, $userinfo->email, $userinfo->encrypt, $days);
} else {
$token = Doo::userToken();
}
unset($userinfo->encrypt);
unset($userinfo->password);
return $userinfo->token;
}
/**
* 判断用户权限(身份)
* @param $identity
* @param $userIdentity
* @return bool
*/
public static function identityRaw($identity, $userIdentity)
{
$userIdentity = is_array($userIdentity) ? $userIdentity : explode(",", trim($userIdentity, ","));
return $identity && in_array($identity, $userIdentity);
return $userinfo->token = $token;
}
/**
@ -410,10 +440,10 @@ class User extends AbstractModel
if (isset($_A["__static_userid2basic_" . $userid])) {
return $_A["__static_userid2basic_" . $userid];
}
$fields = ['userid', 'email', 'nickname', 'profession', 'userimg'];
$userInfo = self::whereUserid($userid)->select($fields)->first();
$userInfo = self::whereUserid($userid)->select(User::$basicField)->first();
if ($userInfo) {
$userInfo->online = $userInfo->getOnlineStatus();
$userInfo->department_name = $userInfo->getDepartmentName();
}
return $_A["__static_userid2basic_" . $userid] = ($userInfo ?: []);
}
@ -426,21 +456,7 @@ class User extends AbstractModel
*/
public static function userid2nickname($userid)
{
$basic = self::userid2basic($userid);
return $basic ? $basic->nickname : '';
}
/**
* 更新首字母
* @param $userid
*/
public static function AZUpdate($userid)
{
$row = self::whereUserid($userid)->first();
if ($row) {
$row->az = Base::getFirstCharter($row->nickname);
$row->save();
}
return self::userid2basic($userid)?->nickname ?: '';
}
/**
@ -467,6 +483,42 @@ class User extends AbstractModel
}
}
/**
* 获取头像
* @param $userid
* @param $userimg
* @param $email
* @param $nickname
* @return string
*/
public static function getAvatar($userid, $userimg, $email, $nickname)
{
// 自定义头像
if ($userimg && !str_contains($userimg, 'avatar/')) {
return Base::fillUrl($userimg);
}
// 机器人头像
switch ($email) {
case 'system-msg@bot.system':
return url("images/avatar/default_system.png");
case 'task-alert@bot.system':
return url("images/avatar/default_task.png");
case 'check-in@bot.system':
return url("images/avatar/default_checkin.png");
case 'anon-msg@bot.system':
return url("images/avatar/default_anon.png");
case 'bot-manager@bot.system':
return url("images/avatar/default_bot.png");
}
// 生成文字头像
if (self::$defaultAvatarMode === 'auto') {
return url("avatar/" . urlencode($nickname) . ".png");
}
// 系统默认头像
$name = ($userid - 1) % 21 + 1;
return url("images/avatar/default_{$name}.png");
}
/**
* 检测密码策略是否符合
* @param $password
@ -497,4 +549,60 @@ class User extends AbstractModel
}
}
}
/**
* 获取机器人或创建
* @param $key
* @param $update
* @param $userid
* @return self|null
*/
public static function botGetOrCreate($key, $update = [], $userid = 0)
{
$email = "{$key}@bot.system";
$botUser = self::whereEmail($email)->first();
if (empty($botUser)) {
$botUser = Doo::userCreate($email, Base::generatePassword(32));
if (empty($botUser)) {
return null;
}
$botUser->updateInstance([
'created_ip' => Base::getIp(),
]);
$botUser->save();
if ($userid > 0) {
UserBot::createInstance([
'userid' => $userid,
'bot_id' => $botUser->userid,
])->save();
}
//
switch ($key) {
case 'system-msg':
$update['nickname'] = '系统消息';
break;
case 'task-alert':
$update['nickname'] = '任务提醒';
break;
case 'check-in':
$update['nickname'] = '签到打卡';
break;
case 'anon-msg':
$update['nickname'] = '匿名消息';
break;
case 'bot-manager':
$update['nickname'] = '机器人管理';
break;
}
}
if ($update) {
$botUser->updateInstance($update);
if (isset($update['nickname'])) {
$botUser->az = Base::getFirstCharter($botUser->nickname);
$botUser->pinyin = Base::cn2pinyin($botUser->nickname);
}
$botUser->save();
}
return $botUser;
}
}

127
app/Models/UserBot.php Normal file
View File

@ -0,0 +1,127 @@
<?php
namespace App\Models;
use App\Module\Doo;
use App\Module\Extranet;
use Cache;
use Carbon\Carbon;
/**
* App\Models\UserBot
*
* @property int $id
* @property int|null $userid 所属人ID
* @property int|null $bot_id 机器人ID
* @property int|null $clear_day 消息自动清理天数
* @property string|null $clear_at 下一次清理时间
* @property string|null $webhook_url 消息webhook地址
* @property int|null $webhook_num 消息webhook请求次数
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserBot newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserBot newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserBot query()
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereBotId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereClearAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereClearDay($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereWebhookNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereWebhookUrl($value)
* @mixin \Eloquent
*/
class UserBot extends AbstractModel
{
/**
* 机器人菜单
* @param $email
* @return array|array[]
*/
public static function quickMsgs($email)
{
return match ($email) {
'check-in@bot.system' => [
[
'key' => 'checkin',
'label' => Doo::translate('我要签到')
], [
'key' => 'it',
'label' => Doo::translate('IT资讯')
], [
'key' => '36ke',
'label' => Doo::translate('36氪')
], [
'key' => '60s',
'label' => Doo::translate('60s读世界')
], [
'key' => 'joke',
'label' => Doo::translate('开心笑话')
], [
'key' => 'soup',
'label' => Doo::translate('心灵鸡汤')
]
],
'anon-msg@bot.system' => [
[
'key' => 'help',
'label' => Doo::translate('使用说明')
], [
'key' => 'privacy',
'label' => Doo::translate('隐私说明')
],
],
'bot-manager@bot.system' => [
[
'key' => '/help',
'label' => Doo::translate('帮助指令')
], [
'key' => '/api',
'label' => Doo::translate('Api接口文档')
], [
'key' => '/list',
'label' => Doo::translate('我的机器人')
],
],
default => [],
};
}
/**
* 签到机器人
* @param $command
* @param $userid
* @return string
*/
public static function checkinBotQuickMsg($command, $userid)
{
if (Cache::get("UserBot::checkinBotQuickMsg:{$userid}") === "yes") {
return "操作频繁!";
}
Cache::put("UserBot::checkinBotQuickMsg:{$userid}", "yes", Carbon::now()->addSecond());
//
$text = match ($command) {
"checkin" => "暂未开放手动签到。",
default => Extranet::checkinBotQuickMsg($command),
};
return $text ?: '维护中...';
}
/**
* 隐私机器人
* @param $command
* @return string
*/
public static function anonBotQuickMsg($command)
{
return match ($command) {
"help" => "使用说明:打开你想要发匿名消息的个人对话,点击输入框右边的 ⊕ 号,选择 <u>匿名消息</u> 即可输入你想要发送的匿名消息内容。",
"privacy" => "匿名消息将通过 <u>匿名消息(机器人)</u> 发送给对方,不会记录你的身份信息。",
default => '',
};
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
/**
* App\Models\UserCheckinMac
*
* @property int $id
* @property int|null $userid 会员id
* @property string|null $mac MAC地址
* @property string|null $remark 备注
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac query()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereMac($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereRemark($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereUserid($value)
* @mixin \Eloquent
*/
class UserCheckinMac extends AbstractModel
{
/**
* 保存mac地址
* @param $userid
* @param $array
* @return mixed
*/
public static function saveMac($userid, $array)
{
return AbstractModel::transaction(function() use ($array, $userid) {
$ids = [];
$list = [];
foreach ($array as $item) {
if (self::whereMac($item['mac'])->where('userid', '!=', $userid)->exists()) {
throw new ApiException("{$item['mac']} 已被其他成员设置");
}
$update = [];
if ($item['remark']) {
$update = [
'remark' => $item['remark']
];
}
$row = self::updateInsert([
'userid' => $userid,
'mac' => $item['mac']
], $update);
if ($row) {
$ids[] = $row->id;
$list[] = $row;
}
}
self::whereUserid($userid)->whereNotIn('id', $ids)->delete();
//
return Base::retSuccess('修改成功', $list);
});
}
}

View File

@ -0,0 +1,134 @@
<?php
namespace App\Models;
use App\Module\Base;
/**
* App\Models\UserCheckinRecord
*
* @property int $id
* @property int|null $userid 会员id
* @property string|null $mac MAC地址
* @property string|null $date 签到日期
* @property array $times 签到时间
* @property int|null $report_time 上报的时间戳
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord query()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereDate($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereMac($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereReportTime($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereTimes($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereUserid($value)
* @mixin \Eloquent
*/
class UserCheckinRecord extends AbstractModel
{
/**
* 签到记录
* @param $value
* @return array
*/
public function getTimesAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
/**
* 获取签到时间
* @param int $userid
* @param array $betweenTimes
* @return array
*/
public static function getTimes(int $userid, array $betweenTimes)
{
$array = [];
$records = self::whereUserid($userid)->whereBetween('created_at', $betweenTimes)->orderBy('id')->get();
/** @var self $record */
foreach ($records as $record) {
$times = array_map(function ($time) {
return preg_replace("/(\d+):(\d+):\d+$/", "$1:$2", $time);
}, $record->times);
if (isset($array[$record->date])) {
$array[$record->date] = array_merge($array[$record->date], $times);
} else {
$array[$record->date] = $times;
}
}
//
foreach ($array as $date => $times) {
$times = array_values(array_filter(array_unique($times)));
$inOrder = [];
foreach ($times as $key => $time) {
$inOrder[$key] = strtotime("2022-01-01 {$time}");
}
array_multisort($inOrder, SORT_ASC, $times);
$array[$date] = $times;
}
//
return $array;
}
/**
* 时间收集
* @param string $data
* @param array $times
* @return \Illuminate\Support\Collection
*/
public static function atCollect($data, $times)
{
$sameTimes = array_map(function($time) use ($data) {
return [
"datetime" => "{$data} {$time}",
"timestamp" => strtotime("{$data} {$time}")
];
}, $times);
return collect($sameTimes);
}
/**
* 签到时段
* @param array $times
* @param int $diff 多长未签到算失效(秒)
* @return array
*/
public static function atSection($times, $diff = 3600)
{
$start = "";
$end = "";
$array = [];
foreach ($times as $time) {
$time = preg_replace("/(\d+):(\d+):\d+$/", "$1:$2", $time);
if (empty($start)) {
$start = $time;
continue;
}
if (empty($end)) {
$end = $time;
continue;
}
if (strtotime("2022-01-01 {$time}") - strtotime("2022-01-01 {$end}") > $diff) {
$array[] = [$start, $end];
$start = $time;
$end = "";
continue;
}
$end = $time;
}
if ($start) {
$array[] = [$start, $end];
}
return $array;
}
}

71
app/Models/UserDelete.php Normal file
View File

@ -0,0 +1,71 @@
<?php
namespace App\Models;
use App\Module\Base;
/**
* App\Models\UserDelete
*
* @property int $id
* @property int|null $operator 操作人员
* @property int|null $userid 用户id
* @property string|null $email 邮箱帐号
* @property string|null $reason 注销原因
* @property string $cache 会员资料缓存
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete query()
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereCache($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereOperator($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereReason($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereUserid($value)
* @mixin \Eloquent
*/
class UserDelete extends AbstractModel
{
/**
* 昵称
* @param $value
* @return string
*/
public function getCacheAttribute($value)
{
if (!is_array($value)) {
$value = Base::json2array($value);
// 昵称
if (!$value['nickname']) {
$value['nickname'] = Base::cardFormat($value['email']);
}
// 头像
$value['userimg'] = User::getAvatar($value['userid'], $value['userimg'], $value['email'], $value['nickname']);
// 部门
$value['department'] = array_filter(is_array($value['department']) ? $value['department'] : Base::explodeInt($value['department']));
}
return $value;
}
/**
* userid 获取 基础信息
* @param int $userid 会员ID
* @return array|null
*/
public static function userid2basic($userid)
{
$row = self::whereUserid($userid)->first();
if (empty($row) || empty($row->cache)) {
return null;
}
$cache = $row->cache;
$cache = array_intersect_key($cache, array_flip(array_merge(User::$basicField, ['department_name'])));
$cache['delete_at'] = $row->created_at->format($row->dateFormat ?: 'Y-m-d H:i:s');
return $cache;
}
}

View File

@ -0,0 +1,152 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
/**
* App\Models\UserDepartment
*
* @property int $id
* @property string|null $name 部门名称
* @property int|null $dialog_id 聊天会话ID
* @property int|null $parent_id 上级部门
* @property int|null $owner_userid 部门负责人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment query()
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereOwnerUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereParentId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereUpdatedAt($value)
* @mixin \Eloquent
*/
class UserDepartment extends AbstractModel
{
/**
* 保存部门
* @param $data
* @param $dialogUseid
*/
public function saveDepartment($data = [], $dialogUseid = 0) {
AbstractModel::transaction(function () use ($dialogUseid, $data) {
$oldUser = null;
$newUser = null;
if ($data['owner_userid'] !== $this->owner_userid) {
$oldUser = User::find($this->owner_userid);
$newUser = User::find($data['owner_userid']);
}
$this->updateInstance($data);
//
if ($this->dialog_id > 0) {
// 已有群
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$dialog->name = $this->name;
$dialog->owner_id = $this->owner_userid;
if ($dialog->save()) {
$dialog->joinGroup($this->owner_userid, 0, true);
$dialog->pushMsg("groupUpdate", [
'id' => $dialog->id,
'name' => $dialog->name,
'owner_id' => $dialog->owner_id,
]);
}
}
} elseif ($dialogUseid > 0) {
// 使用现有群
$dialog = WebSocketDialog::whereType('group')->whereGroupType('user')->find($dialogUseid);
if (empty($dialog)) {
throw new ApiException("选择现有聊天群不存在");
}
$dialog->name = $this->name;
$dialog->owner_id = $this->owner_userid;
$dialog->group_type = 'department';
if ($dialog->save()) {
$dialog->joinGroup($this->owner_userid, 0, true);
$dialog->pushMsg("groupUpdate", [
'id' => $dialog->id,
'name' => $dialog->name,
'owner_id' => $dialog->owner_id,
'group_type' => $dialog->group_type,
]);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'notice', [
'notice' => User::nickname() . " 将此群改为部门群"
], User::userid(), true, true);
}
$this->dialog_id = $dialog->id;
} else {
// 创建群
$dialog = WebSocketDialog::createGroup($this->name, [$this->owner_userid], 'department', $this->owner_userid);
if (empty($dialog)) {
throw new ApiException("创建群组失败");
}
$this->dialog_id = $dialog->id;
}
$this->save();
//
if ($oldUser) {
$oldUser->department = array_diff($oldUser->department, [$this->id]);
$oldUser->department = "," . implode(",", $oldUser->department) . ",";
$oldUser->save();
}
if ($newUser) {
$newUser->department = array_diff($newUser->department, [$this->id]);
$newUser->department = array_merge($newUser->department, [$this->id]);
$newUser->department = "," . implode(",", $newUser->department) . ",";
$newUser->save();
}
});
}
/**
* 删除部门
* @return void
*/
public function deleteDepartment() {
// 删除子部门
$list = self::whereParentId($this->id)->get();
foreach ($list as $item) {
$item->deleteDepartment();
}
// 移出成员
User::where("department", "like", "%,{$this->id},%")->chunk(100, function($items) {
/** @var User $user */
foreach ($items as $user) {
$user->department = array_diff($user->department, [$this->id]);
$user->department = "," . implode(",", $user->department) . ",";
$user->save();
}
});
// 解散群组
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$dialog->deleteDialog();
}
//
$this->delete();
}
/**
* 移交部门身份
* @param $originalUserid
* @param $newUserid
* @return void
*/
public static function transfer($originalUserid, $newUserid)
{
self::whereOwnerUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
/** @var self $item */
foreach ($list as $item) {
$item->saveDepartment([
'owner_userid' => $newUserid,
]);
}
});
}
}

View File

@ -16,6 +16,7 @@ use Guanguans\Notify\Messages\EmailMessage;
* @property string|null $code 验证参数
* @property string|null $email 电子邮箱
* @property int|null $status 0-未验证1-已验证
* @property int|null $type 邮件类型1-邮箱认证2-修改邮箱
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification newModelQuery()
@ -26,6 +27,7 @@ use Guanguans\Notify\Messages\EmailMessage;
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereUserid($value)
* @mixin \Eloquent
@ -36,38 +38,54 @@ class UserEmailVerification extends AbstractModel
/**
* 发验证邮箱
* @param User $user
* @param int $type
* @param null $email
*/
public static function userEmailSend(User $user)
public static function userEmailSend(User $user, $type = 1, $email = null)
{
$res = self::whereUserid($user->userid)->where('created_at', '>', Carbon::now()->subMinutes(30))->first();
if ($res) return;
$email = $type == 1 ? $user->email : $email;
$res = self::whereEmail($email)->where('created_at', '>', Carbon::now()->subMinutes(30))->whereType($type)->first();
if ($res && $type == 1) return;
//删除
self::whereUserid($user->userid)->delete();
$userEmailVerification = self::createInstance([
self::whereUserid($email)->delete();
$code = $type == 1 ? Base::generatePassword(64) : rand(100000, 999999);
$row = self::createInstance([
'userid' => $user->userid,
'email' => $user->email,
'code' => Base::generatePassword(64),
'email' => $email,
'code' => $code,
'status' => 0,
'type' => $type
]);
$userEmailVerification->save();
$row->save();
$setting = Base::setting('emailSetting');
$url = Base::fillUrl('single/valid/email') . '?code=' . $userEmailVerification->code;
try {
if (!Base::isEmail($user->email)) {
throw new \Exception("User email '{$user->email}' address error");
if (!Base::isEmail($email)) {
throw new \Exception("User email '{$email}' address error");
}
switch ($type) {
case 2:
$subject = env('APP_NAME') . "修改邮箱验证";
$content = "<p>{$user->nickname} 您好,您正在修改 " . env('APP_NAME') . " 的邮箱验证码如下。请在30分钟内输入验证码</p><p style='color: #0000DD;'><u>$code</u></p><p>如果不是本人操作,您的帐号可能存在风险,请及时修改密码!</p>";
break;
case 3:
$subject = env('APP_NAME') . "注销帐号验证";
$content = "<p>{$user->nickname} 您好,您正在注销 " . env('APP_NAME') . " 的帐号验证码如下。请在30分钟内输入验证码</p><p style='color: #0000DD;'><u>$code</u></p><p>如果不是本人操作,您的帐号可能存在风险,请及时修改密码!</p>";
break;
default:
$url = Base::fillUrl('single/valid/email') . '?code=' . $row->code;
$subject = env('APP_NAME') . "绑定邮箱验证";
$content = "<p>{$user->nickname} 您好,您正在绑定 " . env('APP_NAME') . " 的邮箱请于30分钟之内点击以下链接完成验证 :</p><p style='display: flex; justify-content: center;'><a href='{$url}' target='_blank'>{$url}</a></p>";
break;
}
$subject = env('APP_NAME') . " 绑定邮箱验证";
$content = "<p>{$user->nickname} 您好,您正在绑定 " . env('APP_NAME') . " 的邮箱请于30分钟之内点击以下链接完成验证 :</p><p style='display: flex; justify-content: center;'><a href='{$url}' target='_blank'>{$url}</a></p>";
Factory::mailer()
->setDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0")
->setMessage(EmailMessage::create()
->from(env('APP_NAME', 'Task') . " <{$setting['account']}>")
->to($user->email)
->to($email)
->subject($subject)
->html($content))
->send();
} catch (\Exception $e) {
} catch (\Throwable $e) {
if (str_contains($e->getMessage(), "Timed Out")) {
throw new ApiException("language.TimedOut");
} elseif ($e->getCode() === 550) {
@ -77,4 +95,39 @@ class UserEmailVerification extends AbstractModel
}
}
}
/**
* 校验验证码
* @param $email
* @param $code
* @param int $type
* @return bool
*/
public static function verify($email, $code, $type = 1)
{
if (!$code) {
throw new ApiException('请输入验证码');
}
/** @var UserEmailVerification $emailVerify */
$emailVerify = self::whereEmail($email)->whereType($type)->orderByDesc('id')->first();
if (empty($emailVerify) || $emailVerify->code != $code) {
throw new ApiException('验证码错误');
}
$oldTime = Carbon::parse($emailVerify->created_at)->timestamp;
$time = Base::Time();
// 30分钟失效
if (abs($time - $oldTime) > 1800) {
throw new ApiException('验证码已失效');
}
self::whereEmail($email)->whereCode($code)->whereType($type)->update([
'status' => 1
]);
return true;
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
/**
* App\Models\UserTransfer
*
* @property int $id
* @property int|null $original_userid 原作者
* @property int|null $new_userid 交接人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer query()
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereNewUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereOriginalUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereUpdatedAt($value)
* @mixin \Eloquent
*/
class UserTransfer extends AbstractModel
{
/**
* 开始移交
* @return void
*/
public function start()
{
// 移交部门
UserDepartment::transfer($this->original_userid, $this->new_userid);
// 移交项目身份
ProjectUser::transfer($this->original_userid, $this->new_userid);
// 移交任务身份
ProjectTaskUser::transfer($this->original_userid, $this->new_userid);
// 移交文件
File::transfer($this->original_userid, $this->new_userid);
// 离职移出群组
WebSocketDialog::select(['web_socket_dialogs.*'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.type', 'group')
->where('u.userid', $this->original_userid)
->orderByDesc('web_socket_dialogs.id')
->chunk(100, function($list) {
/** @var WebSocketDialog $dialog */
foreach ($list as $dialog) {
// 离职员工退出群
$dialog->exitGroup($this->original_userid, 'remove', false, false);
if ($dialog->owner_id === $this->original_userid) {
// 如果是群主则把交接人设为群主
$dialog->owner_id = $this->new_userid;
if ($dialog->save()) {
$dialog->joinGroup($this->new_userid, 0);
$dialog->pushMsg("groupUpdate", [
'id' => $dialog->id,
'owner_id' => $dialog->owner_id,
]);
}
}
}
});
}
}

View File

@ -3,7 +3,12 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use App\Tasks\PushTask;
use Cache;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@ -13,7 +18,9 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property string|null $type 对话类型
* @property string|null $group_type 聊天室类型
* @property string|null $name 对话名称
* @property string $avatar 头像(群)
* @property string|null $last_at 最后消息时间
* @property int|null $owner_id 群主用户ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
@ -23,12 +30,14 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog newQuery()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialog onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereAvatar($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereGroupType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereLastAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereOwnerId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereUpdatedAt($value)
* @method static \Illuminate\Database\Query\Builder|WebSocketDialog withTrashed()
@ -39,6 +48,16 @@ class WebSocketDialog extends AbstractModel
{
use SoftDeletes;
/**
* 头像地址
* @param $value
* @return string
*/
public function getAvatarAttribute($value)
{
return $value ? Base::fillUrl($value) : $value;
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
@ -47,6 +66,236 @@ class WebSocketDialog extends AbstractModel
return $this->hasMany(WebSocketDialogUser::class, 'dialog_id', 'id');
}
/**
* 格式化对话
* @param int $userid 会员ID
* @param bool $hasData
* @return $this
*/
public function formatData($userid, $hasData = false)
{
$dialogUserFun = function ($key, $default = null) use ($userid) {
$data = Cache::remember("Dialog::formatData-{$this->id}-{$userid}", now()->addSeconds(10), function () use ($userid) {
return WebSocketDialogUser::whereDialogId($this->id)->whereUserid($userid)->first()?->toArray();
});
return $data[$key] ?? $default;
};
//
$time = Carbon::parse($this->user_at ?? $dialogUserFun('updated_at'));
$this->top_at = $this->top_at ?? $dialogUserFun('top_at');
$this->user_at = $time->toDateTimeString('millisecond');
$this->user_ms = $time->valueOf();
//
if (isset($this->search_msg_id)) {
// 最后消息 (搜索预览消息)
$this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->find($this->search_msg_id);
$this->last_at = $this->last_msg?->created_at;
} else {
// 未读信息
$this->generateUnread($userid, $hasData);
// 未读标记
$this->mark_unread = $this->mark_unread ?? $dialogUserFun('mark_unread');
// 是否免打扰
$this->silence = $this->silence ?? $dialogUserFun('silence');
// 对话人数
$this->people = WebSocketDialogUser::whereDialogId($this->id)->count();
// 有待办
$this->todo_num = WebSocketDialogMsgTodo::whereDialogId($this->id)->whereUserid($userid)->whereDoneAt(null)->count();
// 最后消息
$this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first();
}
// 对方信息
$this->pinyin = Base::cn2pinyin($this->name);
$this->quick_msgs = [];
$this->dialog_user = null;
$this->group_info = null;
$this->bot = 0;
switch ($this->type) {
case "user":
$dialog_user = WebSocketDialogUser::whereDialogId($this->id)->where('userid', '!=', $userid)->first();
if ($dialog_user->userid === 0) {
$dialog_user->userid = $userid;
}
$basic = User::userid2basic($dialog_user->userid);
if ($basic) {
$this->name = $basic->nickname;
$this->bot = $basic->bot;
$this->quick_msgs = UserBot::quickMsgs($basic->email);
} else {
$this->name = 'non-existent';
$this->dialog_delete = 1;
}
$this->dialog_user = $dialog_user;
break;
case "group":
switch ($this->group_type) {
case 'project':
$this->group_info = Project::withTrashed()->select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($this->id)->first()?->cancelAppend()->cancelHidden();
if ($this->group_info) {
$this->name = $this->group_info->name;
} else {
$this->name = '[Delete]';
$this->dialog_delete = 1;
}
break;
case 'task':
$this->group_info = ProjectTask::withTrashed()->select(['id', 'name', 'complete_at', 'archived_at', 'deleted_at'])->whereDialogId($this->id)->first()?->cancelAppend()->cancelHidden();
if ($this->group_info) {
$this->name = $this->group_info->name;
} else {
$this->name = '[Delete]';
$this->dialog_delete = 1;
}
break;
case 'all':
$this->name = Doo::translate('全体成员');
$this->all_group_mute = Base::settingFind('system', 'all_group_mute');
break;
}
break;
}
if ($hasData === true) {
$msgBuilder = WebSocketDialogMsg::whereDialogId($this->id);
$this->has_tag = $msgBuilder->clone()->where('tag', '>', 0)->exists();
$this->has_image = $msgBuilder->clone()->whereMtype('image')->exists();
$this->has_file = $msgBuilder->clone()->whereMtype('file')->exists();
$this->has_link = $msgBuilder->clone()->whereLink(1)->exists();
}
return $this;
}
/**
* 生成未读数据
* @param $userid
* @param $positionData
* @return $this
*/
public function generateUnread($userid, $positionData = false)
{
$builder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
$this->unread = $builder->count();
$this->mention = $this->unread > 0 ? $builder->clone()->whereMention(1)->count() : 0;
if ($positionData) {
$array = [];
// @我的消息
if ($this->mention > 0
&& $mention_id = intval($builder->clone()->whereMention(1)->orderByDesc('msg_id')->value('msg_id'))) {
$array[] = [
'msg_id' => $mention_id,
'label' => Doo::translate('@我的消息'),
];
}
// 最早一条未读消息
if ($this->unread > 0
&& $first_id = intval($builder->clone()->orderBy('msg_id')->value('msg_id'))) {
$array[] = [
'msg_id' => $first_id,
'label' => '{UNREAD}'
];
}
//
$this->position_msgs = $array;
}
return $this;
}
/**
* 加入聊天室
* @param int|array $userid 加入的会员ID或会员ID组
* @param int $inviter 邀请人
* @param bool|null $important 重要人员(null不修改、bool修改)
* @return bool
*/
public function joinGroup($userid, $inviter, $important = null)
{
AbstractModel::transaction(function () use ($important, $inviter, $userid) {
foreach (is_array($userid) ? $userid : [$userid] as $value) {
if ($value > 0) {
$updateData = [
'inviter' => $inviter,
];
if (is_bool($important)) {
$updateData['important'] = $important ? 1 : 0;
}
$isInsert = false;
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->id,
'userid' => $value,
], $updateData, [], $isInsert);
if ($isInsert) {
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
'notice' => User::userid2nickname($value) . " 已加入群组"
], $inviter, true, true);
}
}
}
});
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
return true;
}
/**
* 退出聊天室
* @param int|array $userid 退出的会员ID或会员ID组
* @param string $type exit|remove
* @param bool $checkDelete 是否检查删除
* @param bool $pushMsg 是否推送消息
*/
public function exitGroup($userid, $type = 'exit', $checkDelete = true, $pushMsg = true)
{
$typeDesc = $type === 'remove' ? '移出' : '退出';
AbstractModel::transaction(function () use ($pushMsg, $checkDelete, $typeDesc, $type, $userid) {
$builder = WebSocketDialogUser::whereDialogId($this->id);
if (is_array($userid)) {
$builder->whereIn('userid', $userid);
} else {
$builder->whereUserid($userid);
}
$builder->chunkById(100, function($list) use ($pushMsg, $checkDelete, $typeDesc, $type) {
/** @var WebSocketDialogUser $item */
foreach ($list as $item) {
if ($checkDelete) {
if ($type === 'remove') {
// 移出时:如果是全员群仅允许管理员操作,其他群仅群主或邀请人可以操作
if ($this->group_type === 'all') {
User::auth("admin");
} elseif (!in_array(User::userid(), [$this->owner_id, $item->inviter])) {
throw new ApiException('只有群主或邀请人可以移出成员');
}
}
if ($item->userid == $this->owner_id) {
throw new ApiException('群主不可' . $typeDesc);
}
if ($item->important) {
throw new ApiException('部门成员、项目人员或任务人员不可' . $typeDesc);
}
}
//
$item->delete();
//
if ($pushMsg) {
if ($type === 'remove') {
$notice = User::nickname() . "" . User::userid2nickname($item->userid) . " 移出群组";
} else {
$notice = User::userid2nickname($item->userid) . " 退出群组";
}
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
'notice' => $notice
], User::userid(), true, true);
}
}
});
});
//
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
}
/**
* 删除会话
* @return bool
@ -61,6 +310,7 @@ class WebSocketDialog extends AbstractModel
});
$this->delete();
});
$this->pushMsg("groupDelete");
return true;
}
@ -68,18 +318,127 @@ class WebSocketDialog extends AbstractModel
* 还原会话
* @return bool
*/
public function recoveryDialog()
public function restoreDialog()
{
$this->restore();
$this->pushMsg("groupRestore");
return true;
}
/**
* 检查群组类型
* @param string|array|null $groupType
* @return void
*/
public function checkGroup($groupType = null)
{
if ($this->type !== 'group') {
throw new ApiException('仅限群组操作');
}
if ($groupType) {
$groupTypes = is_array($groupType) ? $groupType : [$groupType];
if (!in_array($this->group_type, $groupTypes)) {
throw new ApiException('操作的群组类型错误');
}
}
}
/**
* 检查禁言
* @param $userid
* @return void
*/
public function checkMute($userid)
{
if ($this->group_type === 'all') {
$allGroupMute = Base::settingFind('system', 'all_group_mute');
switch ($allGroupMute) {
case 'all':
throw new ApiException('当前会话全员禁言');
case 'user':
if (!User::find($userid)?->checkAdmin()) {
throw new ApiException('当前会话禁言');
}
}
}
}
/**
* 获取群组名称
* @return mixed|string|null
*/
public function getGroupName()
{
if (!isset($this->appendattrs['groupName'])) {
$name = $this->name;
if ($this->type == "group") {
switch ($this->group_type) {
case 'project':
$name = \DB::table('projects')->where('dialog_id', $this->id)->value('name');
break;
case 'task':
$name = \DB::table('project_tasks')->where('dialog_id', $this->id)->value('name');
break;
case 'all':
$name = Doo::translate('全体成员');
break;
}
}
$this->appendattrs['groupName'] = $name;
}
return $this->appendattrs['groupName'];
}
/**
* 推送消息
* @param $action
* @param array $data 发送内容,默认为[id=>会话ID]
* @param array $userid 指定会员,默认为群组所有成员
* @return void
*/
public function pushMsg($action, $data = null, $userid = null)
{
if ($data === null) {
$data = ['id' => $this->id];
}
//
if ($userid === null) {
$userid = $this->dialogUser->pluck('userid')->toArray();
}
//
$params = [
'userid' => $userid,
'msg' => [
'type' => 'dialog',
'mode' => $action,
'data' => $data,
]
];
$task = new PushTask($params, false);
Task::deliver($task);
}
/**
* 更新对话最后消息时间
* @return WebSocketDialogMsg|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder|object|null
*/
public function updateMsgLastAt()
{
$lastMsg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first();
if ($lastMsg) {
$this->last_at = $lastMsg->created_at;
$this->save();
}
return $lastMsg;
}
/**
* 获取对话(同时检验对话身份)
* @param $dialog_id
* @param bool|string $checkOwner 是否校验群组身份,'auto'时有群主为true无群主为false
* @return self
*/
public static function checkDialog($dialog_id)
public static function checkDialog($dialog_id, $checkOwner = false)
{
$dialog = WebSocketDialog::find($dialog_id);
if (empty($dialog)) {
@ -87,7 +446,14 @@ class WebSocketDialog extends AbstractModel
}
//
$userid = User::userid();
if ($dialog->type === 'group' && $dialog->group_type === 'task') {
if ($checkOwner === 'auto') {
$checkOwner = $dialog->owner_id > 0;
}
if ($checkOwner === true && $dialog->owner_id != $userid) {
throw new ApiException('仅限群主操作');
}
//
if ($dialog->group_type === 'task') {
// 任务群对话校验是否在项目内
$project_id = intval(ProjectTask::whereDialogId($dialog->id)->value('project_id'));
if ($project_id > 0) {
@ -97,50 +463,7 @@ class WebSocketDialog extends AbstractModel
}
}
if (!WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->exists()) {
throw new ApiException('不在成员列表内');
}
return $dialog;
}
/**
* 格式化对话
* @param WebSocketDialog $dialog
* @param int $userid 会员ID
* @return self|null
*/
public static function formatData(WebSocketDialog $dialog, $userid)
{
if (empty($dialog)) {
return null;
}
// 最后消息
$last_msg = WebSocketDialogMsg::whereDialogId($dialog->id)->orderByDesc('id')->first();
$dialog->last_msg = $last_msg;
// 未读信息
$dialog->unread = WebSocketDialogMsgRead::whereDialogId($dialog->id)->whereUserid($userid)->whereReadAt(null)->count();
$dialog->mark_unread = $dialog->mark_unread ?? WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->value('mark_unread');
// 对话人数
$builder = WebSocketDialogUser::whereDialogId($dialog->id);
$dialog->people = $builder->count();
// 对方信息
$dialog->dialog_user = null;
$dialog->group_info = null;
$dialog->top_at = $dialog->top_at ?? WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->value('top_at');
switch ($dialog->type) {
case "user":
$dialog_user = $builder->where('userid', '!=', $userid)->first();
$dialog->name = User::userid2nickname($dialog_user->userid);
$dialog->dialog_user = $dialog_user;
break;
case "group":
if ($dialog->group_type === 'project') {
$dialog->group_info = Project::withTrashed()->select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($dialog->id)->first()?->cancelAppend()->cancelHidden();
$dialog->name = $dialog->group_info ? $dialog->group_info->name : '';
} elseif ($dialog->group_type === 'task') {
$dialog->group_info = ProjectTask::withTrashed()->select(['id', 'name', 'complete_at', 'archived_at', 'deleted_at'])->whereDialogId($dialog->id)->first()?->cancelAppend()->cancelHidden();
$dialog->name = $dialog->group_info ? $dialog->group_info->name : '';
}
break;
throw new ApiException('不在成员列表内', ['dialog_id' => $dialog_id], -4003);
}
return $dialog;
}
@ -148,17 +471,20 @@ class WebSocketDialog extends AbstractModel
/**
* 创建聊天室
* @param string $name 聊天室名称
* @param int|array $userid 加入的会员ID或会员ID组
* @param int|array $userid 加入的会员ID()
* @param string $group_type 聊天室类型
* @param int $owner_id 群主会员ID
* @return self|null
*/
public static function createGroup($name, $userid, $group_type = '')
public static function createGroup($name, $userid, $group_type = '', $owner_id = 0)
{
return AbstractModel::transaction(function () use ($userid, $group_type, $name) {
return AbstractModel::transaction(function () use ($owner_id, $userid, $group_type, $name) {
$dialog = self::createInstance([
'type' => 'group',
'name' => $name ?: '',
'group_type' => $group_type,
'owner_id' => $owner_id,
'last_at' => in_array($group_type, ['user', 'department', 'all']) ? Carbon::now() : null,
]);
$dialog->save();
foreach (is_array($userid) ? $userid : [$userid] as $value) {
@ -166,6 +492,7 @@ class WebSocketDialog extends AbstractModel
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $value,
'important' => !in_array($group_type, ['user', 'all'])
])->save();
}
}
@ -173,80 +500,44 @@ class WebSocketDialog extends AbstractModel
});
}
/**
* 加入聊天室
* @param int $dialog_id 会话ID 聊天室ID
* @param int|array $userid 加入的会员ID或会员ID组
* @return bool
*/
public static function joinGroup($dialog_id, $userid)
{
$dialog = self::whereId($dialog_id)->whereType('group')->first();
if (empty($dialog)) {
return false;
}
AbstractModel::transaction(function () use ($dialog, $userid) {
foreach (is_array($userid) ? $userid : [$userid] as $value) {
if ($value > 0) {
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $value,
])->save();
}
}
});
return true;
}
/**
* 退出聊天室
* @param int $dialog_id 会话ID 聊天室ID
* @param int|array $userid 加入的会员ID或会员ID组
* @return bool
*/
public static function exitGroup($dialog_id, $userid)
{
if (is_array($userid)) {
WebSocketDialogUser::whereDialogId($dialog_id)->whereIn('userid', $userid)->delete();
} else {
WebSocketDialogUser::whereDialogId($dialog_id)->whereUserid($userid)->delete();
}
return true;
}
/**
* 获取会员对话(没有自动创建)
* @param int $userid 会员ID
* @param int $userid2 另一个会员ID
* @param User $user 发起会话的会员
* @param int $receiver 另一个会员ID
* @return self|null
*/
public static function checkUserDialog($userid, $userid2)
public static function checkUserDialog($user, $receiver)
{
if ($user->userid == $receiver) {
$receiver = 0;
}
$dialogUser = self::select(['web_socket_dialogs.*'])
->join('web_socket_dialog_users as u1', 'web_socket_dialogs.id', '=', 'u1.dialog_id')
->join('web_socket_dialog_users as u2', 'web_socket_dialogs.id', '=', 'u2.dialog_id')
->where('u1.userid', $userid)
->where('u2.userid', $userid2)
->where('u1.userid', $user->userid)
->where('u2.userid', $receiver)
->where('web_socket_dialogs.type', 'user')
->first();
if ($dialogUser) {
return $dialogUser;
}
return AbstractModel::transaction(function () use ($userid2, $userid) {
if ($receiver > 0 && $user->isTemp()) {
throw new ApiException('无法发起会话');
}
return AbstractModel::transaction(function () use ($receiver, $user) {
$dialog = self::createInstance([
'type' => 'user',
]);
$dialog->save();
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $userid,
'userid' => $user->userid,
])->save();
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $userid2,
'userid' => $receiver,
])->save();
return $dialog;
});
}
}

View File

@ -2,11 +2,12 @@
namespace App\Models;
use App\Exceptions\ApiException;
use Carbon\Carbon;
use App\Models\User;
use App\Module\Base;
use App\Tasks\PushTask;
use App\Exceptions\ApiException;
use App\Tasks\WebSocketDialogMsgTask;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -15,15 +16,26 @@ use Illuminate\Database\Eloquent\SoftDeletes;
*
* @property int $id
* @property int|null $dialog_id 对话ID
* @property string|null $dialog_type 对话类型
* @property int|null $userid 发送会员ID
* @property string|null $type 消息类型
* @property string|null $mtype 消息类型(用于搜索)
* @property array|mixed $msg 详细消息
* @property array|mixed $emoji emoji回复
* @property string|null $key 搜索关键词
* @property int|null $read 已阅数量
* @property int|null $send 发送数量
* @property int|null $tag 标注会员ID
* @property int|null $todo 设为待办会员ID
* @property int|null $link 是否存在链接
* @property int|null $modify 是否编辑
* @property int|null $reply_num 有多少条回复
* @property int|null $reply_id 回复ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read int|mixed $percentage
* @property-read \App\Models\WebSocketDialogMsg|null $reply_data
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newQuery()
@ -32,10 +44,20 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereEmoji($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereKey($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereLink($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereModify($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMsg($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMtype($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereRead($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSend($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTag($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTodo($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUserid($value)
@ -49,9 +71,11 @@ class WebSocketDialogMsg extends AbstractModel
protected $appends = [
'percentage',
'reply_data',
];
protected $hidden = [
'key',
'updated_at',
];
@ -75,6 +99,21 @@ class WebSocketDialogMsg extends AbstractModel
return $this->appendattrs['percentage'];
}
/**
* 回复消息详情
* @return WebSocketDialogMsg|null
*/
public function getReplyDataAttribute()
{
if (!isset($this->appendattrs['reply_data'])) {
$this->appendattrs['reply_data'] = null;
if ($this->reply_id > 0) {
$this->appendattrs['reply_data'] = self::find($this->reply_id, ['id', 'userid', 'type', 'msg'])?->cancelAppend() ?: null;
}
}
return $this->appendattrs['reply_data'];
}
/**
* 消息格式化
* @param $value
@ -90,10 +129,25 @@ class WebSocketDialogMsg extends AbstractModel
$value['type'] = in_array($value['ext'], ['jpg', 'jpeg', 'png', 'gif']) ? 'img' : 'file';
$value['path'] = Base::fillUrl($value['path']);
$value['thumb'] = Base::fillUrl($value['thumb'] ?: Base::extIcon($value['ext']));
} else if ($this->type === 'record') {
$value['path'] = Base::fillUrl($value['path']);
}
return $value;
}
/**
* emoji回复格式化
* @param $value
* @return array|mixed
*/
public function getEmojiAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
/**
* 获取占比
* @param bool|int $increment 是否新增阅读数
@ -157,11 +211,237 @@ class WebSocketDialogMsg extends AbstractModel
return true;
}
/**
* emoji回复
* @param $symbol
* @param int $sender 发送的会员ID
* @return mixed
*/
public function emojiMsg($symbol, $sender)
{
$exist = false;
$array = $this->emoji;
foreach ($array as $index => &$item) {
if ($item['symbol'] === $symbol) {
if (in_array($sender, $item['userids'])) {
// 已存在 去除
$item['userids'] = array_values(array_diff($item['userids'], [$sender]));
if (empty($item['userids'])) {
unset($array[$index]);
$array = array_values($array);
}
} else {
// 未存在 添加
array_unshift($item['userids'], $sender);
}
$exist = true;
break;
}
}
if (!$exist) {
array_unshift($array, [
'symbol' => $symbol,
'userids' => [$sender]
]);
}
//
$this->emoji = Base::array2json($array);
$this->save();
$resData = [
'id' => $this->id,
'emoji' => $array,
];
//
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog?->pushMsg('update', $resData);
//
return Base::retSuccess('success', $resData);
}
/**
* 标注、取消标注
* @param int $sender 标注的会员ID
* @return mixed
*/
public function toggleTagMsg($sender)
{
if (in_array($this->type, ['tag', 'todo', 'notice'])) {
return Base::retError('此消息不支持标注');
}
$before = $this->tag;
$this->tag = $before ? 0 : $sender;
$this->save();
$resData = [
'id' => $this->id,
'tag' => $this->tag,
];
//
$data = [
'update' => $resData
];
$res = self::sendMsg(null, $this->dialog_id, 'tag', [
'action' => $this->tag ? 'add' : 'remove',
'data' => [
'id' => $this->id,
'type' => $this->type,
'msg' => $this->quoteTextMsg(),
]
], $sender);
if (Base::isSuccess($res)) {
$data['add'] = $res['data'];
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog->pushMsg('update', $resData);
} else {
$this->tag = $before;
$this->save();
}
//
return Base::retSuccess($this->tag ? '标注成功' : '取消成功', $data);
}
/**
* 设待办、取消待办
* @param int $sender 设待办的会员ID
* @param array $userids 设置给指定会员
* @return mixed
*/
public function toggleTodoMsg($sender, $userids = [])
{
if (in_array($this->type, ['tag', 'todo', 'notice'])) {
return Base::retError('此消息不支持设待办');
}
if ($this->todo && $this->todo != $sender) {
return Base::retError('仅支持设此待办人员【' . User::userid2nickname($this->todo) . '】取消');
}
$before = $this->todo;
$this->todo = $before ? 0 : $sender;
$this->save();
$resData = [
'id' => $this->id,
'todo' => $this->todo,
];
//
$data = [
'update' => $resData
];
$res = self::sendMsg(null, $this->dialog_id, 'todo', [
'action' => $this->todo ? 'add' : 'remove',
'data' => [
'id' => $this->id,
'type' => $this->type,
'msg' => $this->quoteTextMsg(),
'userids' => implode(",", $userids),
]
], $sender);
if (Base::isSuccess($res)) {
$data['add'] = $res['data'];
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog->pushMsg('update', array_merge($resData, ['dialog_id' => $this->dialog_id]));
//
if ($this->todo) {
$useridList = $dialog->dialogUser->pluck('userid')->toArray();
foreach ($useridList as $userid) {
if ($userids && !in_array($userid, $userids)) {
continue;
}
if (empty($userid)) {
continue;
}
WebSocketDialogMsgTodo::createInstance([
'dialog_id' => $this->dialog_id,
'msg_id' => $this->id,
'userid' => $userid,
])->saveOrIgnore();
}
} else {
WebSocketDialogMsgTodo::whereMsgId($this->id)->delete();
}
} else {
$this->todo = $before;
$this->save();
}
//
return Base::retSuccess($this->todo ? '设置成功' : '取消成功', $data);
}
/**
* 转发消息
* @param array|int $dialogids
* @param array|int $userids
* @param User $user 发送的会员
* @return mixed
*/
public function forwardMsg($dialogids, $userids, $user)
{
return AbstractModel::transaction(function() use ($dialogids, $user, $userids) {
$originalMsg = Base::json2array($this->getRawOriginal('msg'));
$msgs = [];
$already = [];
if ($dialogids) {
if (!is_array($dialogids)) {
$dialogids = [$dialogids];
}
foreach ($dialogids as $dialogid) {
$res = self::sendMsg(null, $dialogid, $this->type, $originalMsg, $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
$already[] = $dialogid;
}
}
}
if ($userids) {
if (!is_array($userids)) {
$userids = [$userids];
}
foreach ($userids as $userid) {
if (!User::whereUserid($userid)->exists()) {
continue;
}
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
if ($dialog && !in_array($dialog->id, $already)) {
$res = self::sendMsg(null, $dialog->id, $this->type, $originalMsg, $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
}
}
}
}
return Base::retSuccess('转发成功', [
'msgs' => $msgs
]);
});
}
/**
* 删除消息
* @param array|int $ids
* @return void
*/
public function deleteMsg()
public static function deleteMsgs($ids) {
$ids = Base::arrayRetainInt(is_array($ids) ? $ids : [$ids], true);
AbstractModel::transaction(function() use ($ids) {
$dialogIds = WebSocketDialogMsg::select('dialog_id')->whereIn("id", $ids)->distinct()->get()->pluck('dialog_id');
$replyIds = WebSocketDialogMsg::select('reply_id')->whereIn("id", $ids)->distinct()->get()->pluck('reply_id');
//
WebSocketDialogMsgRead::whereIn('msg_id', $ids)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可
WebSocketDialogMsgTodo::whereIn('msg_id', $ids)->delete();
self::whereIn('id', $ids)->delete();
//
$dialogDatas = WebSocketDialog::whereIn('id', $dialogIds)->get();
foreach ($dialogDatas as $dialogData) {
$dialogData->updateMsgLastAt();
}
foreach ($replyIds as $id) {
self::whereId($id)->update(['reply_num' => self::whereReplyId($id)->count()]);
}
});
}
/**
* 撤回消息
* @return void
*/
public function withdrawMsg()
{
$send_dt = Carbon::parse($this->created_at)->addDay();
if ($send_dt->lt(Carbon::now())) {
@ -171,16 +451,17 @@ class WebSocketDialogMsg extends AbstractModel
$deleteRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可
$this->delete();
//
$last_msg = null;
if ($this->webSocketDialog) {
$last_msg = WebSocketDialogMsg::whereDialogId($this->dialog_id)->orderByDesc('id')->first();
$this->webSocketDialog->last_at = $last_msg->created_at;
$this->webSocketDialog->save();
if ($this->reply_id > 0) {
self::whereId($this->reply_id)->decrement('reply_num');
}
//
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$userids = $dialog->dialogUser->pluck('userid')->toArray();
$dialogData = $this->webSocketDialog;
if ($dialogData) {
foreach ($dialogData->dialogUser as $dialogUser) {
$dialogUser->updated_at = Carbon::now();
$dialogUser->save();
}
$userids = $dialogData->dialogUser->pluck('userid')->toArray();
PushTask::push([
'userid' => $userids,
'msg' => [
@ -189,43 +470,407 @@ class WebSocketDialogMsg extends AbstractModel
'data' => [
'id' => $this->id,
'dialog_id' => $this->dialog_id,
'last_msg' => $last_msg,
'last_msg' => $dialogData->updateMsgLastAt(),
'update_read' => $deleteRead ? 1 : 0
],
]
]);
}
//
WebSocketDialogMsgTodo::whereMsgId($this->id)->delete();
});
}
/**
* 发送消息
* @param int $dialog_id 会话ID 聊天室ID
* @param string $type 消息类型
* @param array $msg 发送的消息
* @param int $sender 发送的会员ID默认自己0为系统
* 预览消息
* @param bool $preserveHtml 保留html格式
* @param null|array $data
* @return string
*/
public function previewMsg($preserveHtml = false, $data = null)
{
if ($data === null) {
$data = [
'type' => $this->type,
'msg' => $this->msg,
];
}
switch ($data['type']) {
case 'text':
return $this->previewTextMsg($data['msg']['text'], $preserveHtml);
case 'record':
return "[语音]";
case 'meeting':
return "[会议] ${$data['msg']['name']}";
case 'file':
if ($data['msg']['type'] == 'img') {
return "[图片]";
}
return "[文件] {$data['msg']['name']}";
case 'tag':
$action = $data['msg']['action'] === 'remove' ? '取消标注' : '标注';
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
case 'todo':
$action = $data['msg']['action'] === 'remove' ? '取消待办' : ($data['msg']['action'] === 'done' ? '完成' : '设待办');
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
case 'notice':
return $data['msg']['notice'];
default:
return "[未知的消息]";
}
}
/**
* 生成关键词
* @return string
*/
public function generateMsgKey()
{
return match ($this->type) {
'text' => str_replace("&nbsp;", " ", strip_tags($this->msg['text'])),
'meeting', 'file' => $this->msg['name'],
default => '',
};
}
/**
* 返回引用消息(如果是文本消息则截取)
* @param int $strlen
* @return array|mixed
*/
public function quoteTextMsg($strlen = 30)
{
$msg = $this->msg;
if ($this->type === 'text') {
$msg['text'] = $this->previewTextMsg($msg['text']);
if (mb_strlen($msg['text']) > $strlen) {
$msg['text'] = mb_substr($msg['text'], 0, $strlen - 3) . "...";
}
}
return $msg;
}
/**
* 返回文本预览消息
* @param $text
* @param bool $preserveHtml 保留html格式
* @return string|string[]|null
*/
private function previewTextMsg($text, $preserveHtml = false)
{
if (!$text) return '';
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?alt=\"(\S+)\"[^>]*?>/", "[$1]", $text);
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?>/", "[动画表情]", $text);
$text = preg_replace("/<img\s+class=\"browse\"[^>]*?>/", "[图片]", $text);
if (!$preserveHtml) {
$text = strip_tags($text);
$text = str_replace(["&nbsp;", "&amp;", "&lt;", "&gt;"], [" ", "&", "<", ">"], $text);
}
return $text;
}
/**
* 处理文本消息内容,用于发送前
* @param $text
* @param $dialog_id
* @return mixed|string|string[]
*/
public static function formatMsg($text, $dialog_id)
{
@ini_set("pcre.backtrack_limit", 999999999);
// 基础处理
$text = preg_replace("/<(\/[a-zA-Z]+)\s*>/s", "<$1>", $text);
// 图片 [:IMAGE:className:width:height:src:alt:]
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|gif);base64,(.*?)\"(.*?)>(<\/img>)*/s", $text, $matchs);
foreach ($matchs[2] as $key => $base64) {
$imagePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($imagePath));
$imagePath .= md5s($base64) . "." . $matchs[1][$key];
if (file_put_contents(public_path($imagePath), base64_decode($base64))) {
$imageSize = getimagesize(public_path($imagePath));
if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) {
$imagePath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
}
}
// 表情图片
preg_match_all("/<img\s+class=\"emoticon\"(.*?)>/s", $text, $matchs);
foreach ($matchs[1] as $key => $str) {
preg_match("/data-asset=\"(.*?)\"/", $str, $matchAsset);
preg_match("/data-name=\"(.*?)\"/", $str, $matchName);
$imageSize = null;
$imagePath = "";
$imageName = "";
if ($matchAsset[1] === "emosearch") {
preg_match("/src=\"(.*?)\"/", $str, $matchSrc);
if ($matchSrc) {
$srcMd5 = md5($matchSrc[1]);
$imagePath = "uploads/emosearch/" . substr($srcMd5, 0, 2) . "/" . substr($srcMd5, 32 - 2) . "/";
Base::makeDir(public_path($imagePath));
$imagePath .= md5s($matchSrc[1]);
if (file_exists(public_path($imagePath))) {
$imageSize = getimagesize(public_path($imagePath));
} else {
$image = file_get_contents($matchSrc[1]);
if ($image && file_put_contents(public_path($imagePath), $image)) {
$imageSize = getimagesize(public_path($imagePath));
// 添加后缀
if ($imageSize && !str_contains($imagePath, '.')) {
preg_match("/^image\/(png|jpg|jpeg|gif)$/", $imageSize['mime'], $matchMine);
if ($matchMine) {
$imageNewPath = $imagePath . "." . $matchMine[1];
if (rename(public_path($imagePath), public_path($imageNewPath))) {
$imagePath = $imageNewPath;
}
}
}
}
}
}
} elseif (file_exists(public_path($matchAsset[1]))) {
$imagePath = $matchAsset[1];
$imageName = $matchName[1];
$imageSize = getimagesize(public_path($matchAsset[1]));
}
if ($imageSize) {
$text = str_replace($matchs[0][$key], "[:IMAGE:emoticon:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}:{$imageName}:]", $text);
} else {
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text);
}
}
// 其他网络图片
preg_match_all("/<img[^>]*?src=([\"'])(.*?\.(png|jpg|jpeg|gif))\\1[^>]*?>/is", $text, $matchs);
foreach ($matchs[2] as $key => $str) {
if (str_starts_with($str, "{{RemoteURL}}")) {
$imagePath = Base::leftDelete($str, "{{RemoteURL}}");
$imagePath = Base::rightDelete($imagePath, "_thumb.jpg");
} else {
$imagePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($imagePath));
$imagePath .= md5s($str) . "." . $matchs[3][$key];
}
if (file_exists(public_path($imagePath))) {
$imageSize = getimagesize(public_path($imagePath));
if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) {
$imagePath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
} else {
$image = file_get_contents($str);
if (empty($image)) {
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text);
} else if (file_put_contents(public_path($imagePath), $image)) {
$imageSize = getimagesize(public_path($imagePath));
if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) {
$imagePath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
}
}
}
// @成员、#任务、~文件
preg_match_all("/<span\s+class=\"mention\"(.*?)>.*?<\/span>.*?<\/span>.*?<\/span>/s", $text, $matchs);
foreach ($matchs[1] as $key => $str) {
preg_match("/data-denotation-char=\"(.*?)\"/", $str, $matchChar);
preg_match("/data-id=\"(.*?)\"/", $str, $matchId);
preg_match("/data-value=\"(.*?)\"/", $str, $matchValye);
$keyId = $matchId[1];
if ($matchChar[1] === "~") {
if (Base::isNumber($keyId)) {
$file = File::permissionFind($keyId, User::auth());
if ($file->type == 'folder') {
throw new ApiException('文件夹不支持分享');
}
$fileLink = $file->getShareLink(User::userid());
$keyId = $fileLink['code'];
} else {
preg_match("/\/single\/file\/(.*?)$/i", $keyId, $match);
if ($match && strlen($match[1]) >= 8) {
$keyId = $match[1];
} else {
throw new ApiException('文件分享错误');
}
}
}
$text = str_replace($matchs[0][$key], "[:{$matchChar[1]}:{$keyId}:{$matchValye[1]}:]", $text);
}
// 处理快捷消息
preg_match_all("/<span[^>]*?data-quick-key=([\"'])(.*?)\\1[^>]*?>(.*?)<\/span>/is", $text, $matchs);
foreach ($matchs[0] as $key => $str) {
$quickKey = $matchs[2][$key];
$quickLabel = $matchs[3][$key];
if ($quickKey && $quickLabel) {
$quickKey = str_replace(":", "", $quickKey);
$quickLabel = str_replace(":", "", $quickLabel);
$text = str_replace($str, "[:QUICK:{$quickKey}:{$quickLabel}:]", $text);
}
}
// 处理链接标签
preg_match_all("/<a[^>]*?href=([\"'])(.*?)\\1[^>]*?>(.*?)<\/a>/is", $text, $matchs);
foreach ($matchs[0] as $key => $str) {
$herf = $matchs[2][$key];
$title = $matchs[3][$key] ?: $herf;
preg_match("/\/single\/file\/(.*?)$/i", strip_tags($title), $match);
if ($match && strlen($match[1]) >= 8) {
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
if ($file && $file->name) {
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
continue;
}
}
$herf = base64_encode($herf);
$title = base64_encode($title);
$text = str_replace($str, "[:LINK:{$herf}:{$title}:]", $text);
}
// 文件分享链接
preg_match_all("/(https*:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#|@|,|!)+)/i", $text, $matchs);
if ($matchs) {
foreach ($matchs[0] as $str) {
preg_match("/\/single\/file\/(.*?)$/i", $str, $match);
if ($match && strlen($match[1]) >= 8) {
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
if ($file && $file->name) {
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
}
}
}
}
// 过滤标签
$text = strip_tags($text, '<blockquote> <strong> <pre> <ol> <ul> <li> <em> <p> <s> <u> <a>');
$text = preg_replace("/\<(blockquote|strong|pre|ol|ul|li|em|p|s|u).*?\>/is", "<$1>", $text); // 不用去除a标签上面已经处理过了
$text = preg_replace("/\[:IMAGE:(.*?):(.*?):(.*?):(.*?):(.*?):\]/i", "<img class=\"$1\" width=\"$2\" height=\"$3\" src=\"{{RemoteURL}}$4\" alt=\"$5\"/>", $text);
$text = preg_replace("/\[:@:(.*?):(.*?):\]/i", "<span class=\"mention user\" data-id=\"$1\">@$2</span>", $text);
$text = preg_replace("/\[:#:(.*?):(.*?):\]/i", "<span class=\"mention task\" data-id=\"$1\">#$2</span>", $text);
$text = preg_replace("/\[:~:(.*?):(.*?):\]/i", "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/$1\" target=\"_blank\">~$2</a>", $text);
$text = preg_replace("/\[:QUICK:(.*?):(.*?):\]/i", "<span data-quick-key=\"$1\">$2</span>", $text);
$text = preg_replace_callback("/\[:LINK:(.*?):(.*?):\]/i", function (array $match) {
return "<a href=\"" . base64_decode($match[1]) . "\" target=\"_blank\">" . base64_decode($match[2]) . "</a>";
}, $text);
return preg_replace("/^(<p><\/p>)+|(<p><\/p>)+$/i", "", $text);
}
/**
* 发送消息、修改消息
* @param string $action 动作
* - reply-98回复消息ID=98
* - update-99更新消息ID=99
* @param int $dialog_id 会话ID 聊天室ID
* @param string $type 消息类型
* @param array $msg 发送的消息
* @param int $sender 发送的会员ID默认自己0为系统
* @param bool $push_self 推送-是否推给自己
* @param bool $push_retry 推送-失败后重试1次有时候在事务里执行数据还没生成时会出现找不到消息的情况
* @param bool|null $push_silence 推送-静默
* - type = [text|file|record|meeting] 默认为false
* @return array
*/
public static function sendMsg($dialog_id, $type, $msg, $sender = 0)
public static function sendMsg($action, $dialog_id, $type, $msg, $sender = 0, $push_self = false, $push_retry = false, $push_silence = null)
{
$dialogMsg = self::createInstance([
'userid' => $sender ?: User::userid(),
'type' => $type,
'msg' => $msg,
'read' => 0,
]);
AbstractModel::transaction(function () use ($dialog_id, $msg, $dialogMsg) {
$dialog = WebSocketDialog::find($dialog_id);
if (empty($dialog)) {
throw new ApiException('获取会话失败');
$link = 0;
$mtype = $type;
if ($type === 'text') {
if (str_contains($msg['text'], '<a ') || preg_match("/https*:\/\//", $msg['text'])) {
$link = 1;
}
$dialog->last_at = Carbon::now();
$dialog->save();
$dialogMsg->send = 1;
$dialogMsg->dialog_id = $dialog->id;
if (str_contains($msg['text'], '<img ')) {
$mtype = str_contains($msg['text'], '"emoticon"') ? 'emoticon' : 'image';
}
preg_match_all("/@([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,6})/i", $msg['text'], $matchs);
foreach($matchs[0] as $key => $item) {
$aiUser = User::whereEmail($matchs[1][$key])->whereDisableAt(null)->first();
if ($aiUser) {
$msg['text'] = str_replace($item, "<span class=\"mention user\" data-id=\"{$aiUser->userid}\">@{$aiUser->nickname}</span>", $msg['text']);
}
}
} elseif ($type === 'file') {
if (in_array($msg['ext'], ['jpg', 'jpeg', 'png', 'gif'])) {
$mtype = 'image';
}
}
if ($push_silence === null) {
$push_silence = !in_array($type, ["text", "file", "record", "meeting"]);
}
//
$update_id = preg_match("/^update-(\d+)$/", $action, $match) ? $match[1] : 0;
$reply_id = preg_match("/^reply-(\d+)$/", $action, $match) ? $match[1] : 0;
$sender = $sender ?: User::userid();
//
$dialog = WebSocketDialog::find($dialog_id);
if (empty($dialog)) {
throw new ApiException('获取会话失败');
}
$dialog->checkMute($sender);
//
if ($update_id) {
// 修改
$dialogMsg = self::whereId($update_id)->whereDialogId($dialog_id)->first();
if (empty($dialogMsg)) {
throw new ApiException('消息不存在');
}
if ($dialogMsg->type !== 'text') {
throw new ApiException('此消息不支持此操作');
}
if ($dialogMsg->userid != $sender) {
throw new ApiException('仅支持修改自己的消息');
}
//
$updateData = [
'mtype' => $mtype,
'link' => $link,
'msg' => $msg,
'modify' => 1,
];
$dialogMsg->updateInstance($updateData);
$dialogMsg->key = $dialogMsg->generateMsgKey();
$dialogMsg->save();
});
Task::deliver(new WebSocketDialogMsgTask($dialogMsg->id));
return Base::retSuccess('发送成功', $dialogMsg);
//
$dialog->pushMsg('update', array_merge($updateData, [
'id' => $dialogMsg->id
]));
//
return Base::retSuccess('修改成功', $dialogMsg);
} else {
// 发送
if ($reply_id && !self::whereId($reply_id)->increment('reply_num')) {
throw new ApiException('回复的消息不存在');
}
//
$dialogMsg = self::createInstance([
'dialog_id' => $dialog_id,
'dialog_type' => $dialog->type,
'reply_id' => $reply_id,
'userid' => $sender,
'type' => $type,
'mtype' => $mtype,
'link' => $link,
'msg' => $msg,
'read' => 0,
]);
AbstractModel::transaction(function () use ($dialog, $dialogMsg) {
$dialog->last_at = Carbon::now();
$dialog->save();
$dialogMsg->send = 1;
$dialogMsg->key = $dialogMsg->generateMsgKey();
$dialogMsg->save();
WebSocketDialogUser::whereDialogId($dialog->id)->change(['updated_at' => Carbon::now()->toDateTimeString('millisecond')]);
});
//
$task = new WebSocketDialogMsgTask($dialogMsg->id);
if ($push_self) {
$task->setIgnoreFd(null);
}
if ($push_retry) {
$task->setMsgNotExistRetry(true);
}
if ($push_silence) {
$task->setSilence($push_silence);
}
Task::deliver($task);
//
return Base::retSuccess('发送成功', $dialogMsg);
}
}
}

View File

@ -10,7 +10,10 @@ use Carbon\Carbon;
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $msg_id 消息ID
* @property int|null $userid 发送会员ID
* @property int|null $userid 接收会员ID
* @property int|null $mention 是否提及(被@
* @property int|null $silence 是否免打扰0否1是
* @property int|null $email 是否发了邮件
* @property int|null $after 在阅读之后才添加的记录
* @property string|null $read_at 阅读时间
* @property-read \App\Models\WebSocketDialogMsg|null $webSocketDialogMsg
@ -19,9 +22,12 @@ use Carbon\Carbon;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereAfter($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereMention($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereMsgId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereReadAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereSilence($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereUserid($value)
* @mixin \Eloquent
*/

View File

@ -0,0 +1,47 @@
<?php
namespace App\Models;
/**
* App\Models\WebSocketDialogMsgTodo
*
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $msg_id 消息ID
* @property int|null $userid 接收会员ID
* @property string|null $done_at 完成时间
* @property-read array|mixed $msg_data
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereDoneAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereMsgId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereUserid($value)
* @mixin \Eloquent
*/
class WebSocketDialogMsgTodo extends AbstractModel
{
protected $appends = [
'msg_data',
];
function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->timestamps = false;
}
/**
* 消息详情
* @return array|mixed
*/
public function getMsgDataAttribute()
{
if (!isset($this->appendattrs['msgData'])) {
$this->appendattrs['msgData'] = WebSocketDialogMsg::select(['id', 'type', 'msg'])->whereId($this->msg_id)->first()?->cancelAppend();
}
return $this->appendattrs['msgData'];
}
}

View File

@ -2,6 +2,8 @@
namespace App\Models;
use Carbon\Carbon;
/**
* App\Models\WebSocketDialogUser
*
@ -10,15 +12,22 @@ namespace App\Models;
* @property int|null $userid 会员ID
* @property string|null $top_at 置顶时间
* @property int|null $mark_unread 是否标记为未读0否1是
* @property int|null $silence 是否免打扰0否1是
* @property int|null $inviter 邀请人
* @property int|null $important 是否不可移出(项目、任务、部门人员)
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereImportant($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereInviter($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereMarkUnread($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereSilence($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereTopAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUserid($value)
@ -26,5 +35,13 @@ namespace App\Models;
*/
class WebSocketDialogUser extends AbstractModel
{
protected $dateFormat = 'Y-m-d H:i:s.v';
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function webSocketDialog(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(WebSocketDialog::class, 'id', 'dialog_id');
}
}

23
app/Models/clearHelper.php Executable file
View File

@ -0,0 +1,23 @@
<?php
/**
* 清除模型class注释
*/
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
$path = dirname(__FILE__). '/';
$lists = scandir($path);
//
foreach ($lists AS $item) {
$fillPath = $path . $item;
if (!in_array($item, ['AbstractModel.php', 'clearHelper.php']) && str_ends_with($item, '.php')) {
$content = file_get_contents($fillPath);
preg_match("/\/\*\*([\s\S]*?)class\s*(.*?)\s*extends\s*AbstractModel/i", $content, $matchs);
if ($matchs[0]) {
$content = str_replace($matchs[0], 'class ' . $matchs[2] . ' extends AbstractModel', $content);
file_put_contents($fillPath, $content);
}
}
}
echo "Success \n";

View File

@ -0,0 +1,193 @@
<?php
/**
* @Description :
*
* @Date : 2019-03-14 13:22
* @Author : hmy940118@gmail.com
*/
namespace App\Module\AgoraIO;
class AccessToken
{
const Privileges = array(
"kJoinChannel" => 1,
"kPublishAudioStream" => 2,
"kPublishVideoStream" => 3,
"kPublishDataStream" => 4,
"kPublishAudioCdn" => 5,
"kPublishVideoCdn" => 6,
"kRequestPublishAudioStream" => 7,
"kRequestPublishVideoStream" => 8,
"kRequestPublishDataStream" => 9,
"kInvitePublishAudioStream" => 10,
"kInvitePublishVideoStream" => 11,
"kInvitePublishDataStream" => 12,
"kAdministrateChannel" => 101
);
public $appID, $appCertificate, $channelName, $uid;
public $message;
/**
* AccessToken constructor.
* @throws \Exception
*/
public function __construct()
{
$this->message = new Message();
}
/**
* @param $uid
*/
public function setUid($uid)
{
if ($uid === 0) {
$this->uid = "";
} else {
$this->uid = $uid . '';
}
}
/**
* @param $name
* @param $str
* @return bool
*/
public function is_nonempty_string($name, $str)
{
if (is_string($str) && $str !== "") {
return true;
}
echo $name . " check failed, should be a non-empty string";
return false;
}
/**
* @param $appID
* @param $appCertificate
* @param $channelName
* @param $uid
* @return AccessToken|null
* @throws \Exception
*/
public static function init($appID, $appCertificate, $channelName, $uid)
{
$accessToken = new AccessToken();
if (!$accessToken->is_nonempty_string("appID", $appID) ||
!$accessToken->is_nonempty_string("appCertificate", $appCertificate) ||
!$accessToken->is_nonempty_string("channelName", $channelName)) {
return null;
}
$accessToken->appID = $appID;
$accessToken->appCertificate = $appCertificate;
$accessToken->channelName = $channelName;
$accessToken->setUid($uid);
$accessToken->message = new Message();
return $accessToken;
}
/**
* @param $token
* @param $appCertificate
* @param $channel
* @param $uid
* @return AccessToken|null
* @throws \Exception
*/
public static function initWithToken($token, $appCertificate, $channel, $uid)
{
$accessToken = new AccessToken();
if (!$accessToken->extract($token, $appCertificate, $channel, $uid)) {
return null;
}
return $accessToken;
}
/**
* @param $key
* @param $expireTimestamp
* @return $this
*/
public function addPrivilege($key, $expireTimestamp)
{
$this->message->privileges[$key] = $expireTimestamp;
return $this;
}
/**
* @param $token
* @param $appCertificate
* @param $channelName
* @param $uid
* @return bool
* @throws \Exception
*/
protected function extract($token, $appCertificate, $channelName, $uid)
{
$ver_len = 3;
$appid_len = 32;
$version = substr($token, 0, $ver_len);
if ($version !== "006") {
echo 'invalid version ' . $version;
return false;
}
if (!$this->is_nonempty_string("token", $token) ||
!$this->is_nonempty_string("appCertificate", $appCertificate) ||
!$this->is_nonempty_string("channelName", $channelName)) {
return false;
}
$appid = substr($token, $ver_len, $appid_len);
$content = (base64_decode(substr($token, $ver_len + $appid_len, strlen($token) - ($ver_len + $appid_len))));
$pos = 0;
$len = unpack("v", $content . substr($pos, 2))[1];
$pos += 2;
$sig = substr($content, $pos, $len);
$pos += $len;
$crc_channel = unpack("V", substr($content, $pos, 4))[1];
$pos += 4;
$crc_uid = unpack("V", substr($content, $pos, 4))[1];
$pos += 4;
$msgLen = unpack("v", substr($content, $pos, 2))[1];
$pos += 2;
$msg = substr($content, $pos, $msgLen);
$this->appID = $appid;
$message = new Message();
$message->unpackContent($msg);
$this->message = $message;
//non reversable values
$this->appCertificate = $appCertificate;
$this->channelName = $channelName;
$this->setUid($uid);
return true;
}
/**
* @return string
*/
public function build()
{
$msg = $this->message->packContent();
$val = array_merge(unpack("C*", $this->appID), unpack("C*", $this->channelName), unpack("C*", $this->uid), $msg);
$sig = hash_hmac('sha256', implode(array_map("chr", $val)), $this->appCertificate, true);
$crc_channel_name = crc32($this->channelName) & 0xffffffff;
$crc_uid = crc32($this->uid) & 0xffffffff;
$content = array_merge(unpack("C*", $this->packString($sig)), unpack("C*", pack("V", $crc_channel_name)), unpack("C*", pack("V", $crc_uid)), unpack("C*", pack("v", count($msg))), $msg);
$version = "006";
$ret = $version . $this->appID . base64_encode(implode(array_map("chr", $content)));
return $ret;
}
/**
* @param $value
* @return string
*/
public function packString($value)
{
return pack("v", strlen($value)) . $value;
}
}

View File

@ -0,0 +1,122 @@
<?php
/**
* @Description :
*
* @Date : 2019-03-14 13:20
* @Author : hmy940118@gmail.com
*/
namespace App\Module\AgoraIO;
class AgoraTokenGenerator
{
const AttendeePrivileges = array(
AccessToken::Privileges["kJoinChannel"] => 0,
AccessToken::Privileges["kPublishAudioStream"] => 0,
AccessToken::Privileges["kPublishVideoStream"] => 0,
AccessToken::Privileges["kPublishDataStream"] => 0
);
const PublisherPrivileges = array(
AccessToken::Privileges["kJoinChannel"] => 0,
AccessToken::Privileges["kPublishAudioStream"] => 0,
AccessToken::Privileges["kPublishVideoStream"] => 0,
AccessToken::Privileges["kPublishDataStream"] => 0,
AccessToken::Privileges["kPublishAudioCdn"] => 0,
AccessToken::Privileges["kPublishVideoCdn"] => 0,
AccessToken::Privileges["kInvitePublishAudioStream"] => 0,
AccessToken::Privileges["kInvitePublishVideoStream"] => 0,
AccessToken::Privileges["kInvitePublishDataStream"] => 0
);
const SubscriberPrivileges = array(
AccessToken::Privileges["kJoinChannel"] => 0,
AccessToken::Privileges["kRequestPublishAudioStream"] => 0,
AccessToken::Privileges["kRequestPublishVideoStream"] => 0,
AccessToken::Privileges["kRequestPublishDataStream"] => 0
);
const AdminPrivileges = array(
AccessToken::Privileges["kJoinChannel"] => 0,
AccessToken::Privileges["kPublishAudioStream"] => 0,
AccessToken::Privileges["kPublishVideoStream"] => 0,
AccessToken::Privileges["kPublishDataStream"] => 0,
AccessToken::Privileges["kAdministrateChannel"] => 0
);
const Role = array(
"kRoleAttendee" => 0, // for communication
"kRolePublisher" => 1, // for live broadcast
"kRoleSubscriber" => 2, // for live broadcast
"kRoleAdmin" => 101
);
const RolePrivileges = array(
self::Role["kRoleAttendee"] => self::AttendeePrivileges,
self::Role["kRolePublisher"] => self::PublisherPrivileges,
self::Role["kRoleSubscriber"] => self::SubscriberPrivileges,
self::Role["kRoleAdmin"] => self::AdminPrivileges
);
public $token;
/**
* AgoraTokenGenerator constructor.
* @param $appID
* @param $appCertificate
* @param $channelName
* @param $uid
* @throws \Exception
*/
public function __construct($appID, $appCertificate, $channelName, $uid){
$this->token = new AccessToken();
$this->token->appID = $appID;
$this->token->appCertificate = $appCertificate;
$this->token->channelName = $channelName;
$this->token->setUid($uid);
}
/**
* @param $token
* @param $appCertificate
* @param $channel
* @param $uid
* @throws \Exception
*/
public function initWithToken($token, $appCertificate, $channel, $uid){
$this->token = AccessToken::initWithToken($token, $appCertificate, $channel, $uid);
}
/**
* @param $role
*/
public function initPrivilege($role){
$p = self::RolePrivileges[$role];
foreach($p as $key => $value){
$this->setPrivilege($key, $value);
}
}
/**
* @param $privilege
* @param $expireTimestamp
*/
public function setPrivilege($privilege, $expireTimestamp){
$this->token->addPrivilege($privilege, $expireTimestamp);
}
/**
* @param $privilege
*/
public function removePrivilege($privilege){
unset($this->token->message->privileges[$privilege]);
}
/**
* @return string
*/
public function buildToken(){
return $this->token->build();
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* @Description :
*
* @Date : 2019-03-14 13:27
* @Author : hmy940118@gmail.com
*/
namespace App\Module\AgoraIO;
class Message
{
public $salt;
public $ts;
public $privileges;
/**
* Message constructor.
* @throws \Exception
*/
public function __construct()
{
$this->salt = rand(0, 100000);
$date = new \DateTime("now", new \DateTimeZone('UTC'));
$this->ts = $date->getTimestamp() + 168 * 3600; // 7天时间
$this->privileges = array();
}
/**
* @return array
*/
public function packContent()
{
$buffer = unpack("C*", pack("V", $this->salt));
$buffer = array_merge($buffer, unpack("C*", pack("V", $this->ts)));
$buffer = array_merge($buffer, unpack("C*", pack("v", sizeof($this->privileges))));
foreach ($this->privileges as $key => $value) {
$buffer = array_merge($buffer, unpack("C*", pack("v", $key)));
$buffer = array_merge($buffer, unpack("C*", pack("V", $value)));
}
return $buffer;
}
/**
* @param $msg
*/
public function unpackContent($msg)
{
$pos = 0;
$salt = unpack("V", substr($msg, $pos, 4))[1];
$pos += 4;
$ts = unpack("V", substr($msg, $pos, 4))[1];
$pos += 4;
$size = unpack("v", substr($msg, $pos, 2))[1];
$pos += 2;
$privileges = array();
for ($i = 0; $i < $size; $i++) {
$key = unpack("v", substr($msg, $pos, 2));
$pos += 2;
$value = unpack("V", substr($msg, $pos, 4));
$pos += 4;
$privileges[$key[1]] = $value[1];
}
$this->salt = $salt;
$this->ts = $ts;
$this->privileges = $privileges;
}
}

View File

@ -6,11 +6,10 @@ use App\Exceptions\ApiException;
use App\Models\Setting;
use App\Models\Tmp;
use Cache;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Overtrue\Pinyin\Pinyin;
use Redirect;
use Request;
use Response;
use Storage;
use Validator;
@ -76,6 +75,16 @@ class Base
});
}
/**
* 如果header没有则通过input读取
* @param $key
* @return mixed|string
*/
public static function headerOrInput($key)
{
return Base::nullShow(Request::header($key), Request::input($key));
}
/**
* 获取版本号
* @return string
@ -94,7 +103,7 @@ class Base
{
global $_A;
if (!isset($_A["__static_client_version"])) {
$_A["__static_client_version"] = Request::header('version') ?: '0.0.1';
$_A["__static_client_version"] = self::headerOrInput('version') ?: '0.0.1';
}
return $_A["__static_client_version"];
}
@ -106,11 +115,21 @@ class Base
*/
public static function checkClientVersion($min)
{
if (version_compare(Base::getClientVersion(), $min, '<')) {
throw new ApiException('当前版本 (v' . Base::getClientVersion() . ') 过低');
if (!self::judgeClientVersion($min)) {
throw new ApiException('当前版本 (v' . Base::getClientVersion() . ') 过低,最低版本要求 (v' . $min . ')。');
}
}
/**
* 判断客户端版本
* @param $min
* @return bool
*/
public static function judgeClientVersion($min)
{
return !version_compare(Base::getClientVersion(), $min, '<');
}
/**
* 判断是否域名格式
* @param $domain
@ -285,7 +304,7 @@ class Base
{
try {
Storage::makeDirectory($path);
} catch (Exception $e) {
} catch (\Throwable $e) {
}
if (!file_exists($path)) {
self::makeDir(dirname($path));
@ -486,7 +505,7 @@ class Base
try {
$array = json_decode($string, true);
return is_array($array) ? $array : [];
} catch (Exception $e) {
} catch (\Throwable) {
return [];
}
}
@ -504,7 +523,7 @@ class Base
}
try {
return json_encode($array, $options);
} catch (Exception $e) {
} catch (\Throwable) {
return '';
}
}
@ -717,24 +736,20 @@ class Base
/**
* 判断两个地址域名是否相同
* @param string $var1
* @param string $var2
* @param string|array $var2
* @return bool
*/
public static function hostContrast($var1, $var2)
{
$arr1 = parse_url($var1);
$arr2 = parse_url($var2);
$host1 = $arr1['host'] ?? $var1;
//
$host1 = $var1;
if (isset($arr1['host'])) {
$host1 = $arr1['host'];
$host2 = [];
foreach (is_array($var2) ? $var2 : [$var2] as $url) {
$arr2 = parse_url($url);
$host2[] = $arr2['host'] ?? $url;
}
//
$host2 = $var2;
if (isset($arr2['host'])) {
$host2 = $arr2['host'];
}
return $host1 == $host2;
return in_array($host1, $host2);
}
/**
@ -877,36 +892,61 @@ class Base
* 打散字符串,只留为数字的项
* @param $delimiter
* @param $string
* @param bool $reInt 是否格式化值
* @return array
*/
public static function explodeInt($delimiter, $string = null)
public static function explodeInt($delimiter, $string = null, $reInt = true)
{
if ($string == null) {
$string = $delimiter;
$delimiter = ',';
}
$array = is_array($string) ? $string : explode($delimiter, $string);
return self::arrayRetainInt($array);
return self::arrayRetainInt($array, $reInt);
}
/**
* 数组只保留数字的
* @param $array
* @param bool $int 是否格式化值
* @param bool $reInt 是否格式化值
* @return array
*/
public static function arrayRetainInt($array, $int = false)
public static function arrayRetainInt($array, $reInt = false)
{
if (!is_array($array)) {
return $array;
}
foreach ($array as $k => $v) {
if (!is_numeric($v)) {
unset($array[$k]);
} elseif ($int === true) {
} elseif ($reInt === true) {
$array[$k] = intval($v);
}
}
return array_values($array);
}
/**
* 数组拼接字符串(前后也加上)
* @param $glue
* @param $pieces
* @param $around
* @return string
*/
public static function arrayImplode($glue = "", $pieces = null, $around = true)
{
if ($pieces == null) {
$pieces = $glue;
$glue = ',';
}
$pieces = array_values(array_filter(array_unique($pieces)));
$string = implode($glue, $pieces);
if ($around && $string) {
$string = ",{$string},";
}
return $string;
}
/**
* 判断是否二维数组
* @param $array
@ -1015,7 +1055,21 @@ class Base
*/
public static function isNumber($str)
{
if (preg_match("/^\d*$/", $str)) {
if (preg_match("/^\d+$/", $str)) {
return true;
} else {
return false;
}
}
/**
* 正则判断是否MAC地址
* @param $str
* @return bool
*/
public static function isMac($str)
{
if (preg_match("/^[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}:[A-Fa-f\d]{2}$/", $str)) {
return true;
} else {
return false;
@ -1256,6 +1310,27 @@ class Base
return $setting;
}
/**
* 时间转毫秒时间戳
* @param $time
* @return float|int
*/
public static function strtotimeM($time)
{
if (str_contains($time, '.')) {
list($t, $m) = explode(".", $time);
if (is_string($t)) {
$t = strtotime($t);
}
$time = $t . str_pad($m, 3, "0", STR_PAD_LEFT);
}
if (is_numeric($time)) {
return (int) str_pad($time, 13, "0");
} else {
return strtotime($time) * 1000;
}
}
/**
* 获取设置值
* @param $setname
@ -1383,54 +1458,6 @@ class Base
}
}
/**
* 国际化(替换国际语言)
* @param $val
* @return mixed
*/
public static function Lang($val)
{
$repArray = [];
if (is_array($val)) {
if (self::strExists($val[0], '%') && count($val) > 1) {
$repArray = array_slice($val, 1);
}
$val = $val[0];
}
$data = self::langData();
if (isset($data[$val]) && $data[$val] !== null) {
$val = $data[$val];
}
if ($repArray) {
foreach ($repArray as $item) {
$val = self::strReplaceLimit('%', $item, $val, 1);
}
}
return $val;
}
/**
* 加载语言数据
* @param bool $refresh
* @return array
*/
public static function langData($refresh = false)
{
global $_A;
if (!isset($_A["__static_langdata"]) || $refresh === true) {
$_A["__static_langdata"] = [];
$language = trim(Request::header('language'));
$langpath = resource_path('lang/' . $language . '/general.php');
if (file_exists($langpath)) {
$data = include $langpath;
if (is_array($data)) {
$_A["__static_langdata"] = $data;
}
}
}
return $_A["__static_langdata"];
}
/**
* JSON返回
* @param $param
@ -1438,10 +1465,8 @@ class Base
*/
public static function jsonEcho($param)
{
global $_GPC;
//
$json = json_encode($param);
$callback = $_GPC['callback'];
$callback = Request::input('callback');
if ($callback) {
return $callback . '(' . $json . ')';
} else {
@ -1458,11 +1483,11 @@ class Base
*/
public static function retSuccess($msg, $data = [], $ret = 1)
{
return array(
return [
'ret' => $ret,
'msg' => self::Lang($msg),
'msg' => Doo::translate($msg),
'data' => $data
);
];
}
/**
@ -1474,11 +1499,11 @@ class Base
*/
public static function retError($msg, $data = [], $ret = 0)
{
return array(
return [
'ret' => $ret,
'msg' => self::Lang($msg),
'msg' => Doo::translate($msg),
'data' => $data
);
];
}
/**
@ -1632,7 +1657,7 @@ class Base
}
/**
* 用户名、邮箱、手机号、银行卡号中间字符串以*隐藏
* 用户名、邮箱、手机号、银行卡号中间字符串以*隐藏
* @param $str
* @return string
*/
@ -1814,116 +1839,11 @@ class Base
$onlineip = '0,0,0,0';
}
preg_match("/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/", $onlineip, $match);
$_A["__static_ip"] = $match[0] ?: 'unknown';
$_A["__static_ip"] = $match ? ($match[0] ?: 'unknown') : '';
}
return $_A["__static_ip"];
}
/**
* 获取IP地址经纬度
* @param string $ip
* @return array|mixed
*/
public static function getIpGcj02($ip = '')
{
if (empty($ip)) {
$ip = self::getIp();
}
$cacheKey = "getIpPoint::" . md5($ip);
$result = Cache::rememberForever($cacheKey, function () use ($ip) {
return Ihttp::ihttp_request("https://www.ifreesite.com/ipaddress/address.php?q=" . $ip, [], [], 12);
});
if (Base::isError($result)) {
Cache::forget($cacheKey);
return $result;
}
$data = $result['data'];
$lastPos = strrpos($data, ',');
$long = floatval(Base::getMiddle(substr($data, $lastPos + 1), null, ')'));
$lat = floatval(Base::getMiddle(substr($data, strrpos(substr($data, 0, $lastPos), ',') + 1), null, ','));
return Base::retSuccess("success", [
'long' => $long,
'lat' => $lat,
]);
}
/**
* 百度接口根据ip获取经纬度
* @param string $ip
* @return array|mixed
*/
public static function getIpGcj02ByBaidu($ip = ''): array
{
if (empty($ip)) {
$ip = self::getIp();
}
$cacheKey = "getIpPoint::" . md5($ip);
$result = Cache::rememberForever($cacheKey, function () use ($ip) {
$ak = Config::get('app.baidu_app_key');
$url = 'http://api.map.baidu.com/location/ip?ak=' . $ak . '&ip=' . $ip . '&coor=bd09ll';
return Ihttp::ihttp_request($url, [], [], 12);
});
if (Base::isError($result)) {
Cache::forget($cacheKey);
return $result;
}
$data = json_decode($result['data'], true);
// x坐标纬度, y坐标经度
$long = Arr::get($data, 'content.point.x');
$lat = Arr::get($data, 'content.point.y');
return Base::retSuccess("success", [
'long' => $long,
'lat' => $lat,
]);
}
/**
* 获取IP地址详情
* @param string $ip
* @return array|mixed
*/
public static function getIpInfo($ip = '')
{
if (empty($ip)) {
$ip = self::getIp();
}
$cacheKey = "getIpInfo::" . md5($ip);
$result = Cache::rememberForever($cacheKey, function () use ($ip) {
return Ihttp::ihttp_request("http://ip.taobao.com/service/getIpInfo.php?accessKey=alibaba-inc&ip=" . $ip, [], [], 12);
});
if (Base::isError($result)) {
Cache::forget($cacheKey);
return $result;
}
$data = json_decode($result['data'], true);
if (!is_array($data) || intval($data['code']) != 0) {
Cache::forget($cacheKey);
return Base::retError("error ip: -1");
}
$data = $data['data'];
if (!is_array($data) || !isset($data['country'])) {
return Base::retError("error ip: -2");
}
$data['text'] = $data['country'];
$data['textSmall'] = $data['country'];
if ($data['region'] && $data['region'] != $data['country'] && $data['region'] != "XX") {
$data['text'] .= " " . $data['region'];
$data['textSmall'] = $data['region'];
}
if ($data['city'] && $data['city'] != $data['region'] && $data['city'] != "XX") {
$data['text'] .= " " . $data['city'];
$data['textSmall'] .= " " . $data['city'];
}
if ($data['county'] && $data['county'] != $data['city'] && $data['county'] != "XX") {
$data['text'] .= " " . $data['county'];
$data['textSmall'] .= " " . $data['county'];
}
return Base::retSuccess("success", $data);
}
/**
* 是否是中国IP-1错误、1是、0否
* @param string $ip
@ -2099,29 +2019,6 @@ class Base
return $array;
}
/**
* 获取tonken
* @return string
*/
public static function getToken()
{
global $_A;
if (!isset($_A["__static_token"])) {
$_A["__static_token"] = Base::nullShow(Request::header('token'), Request::input('token'));
}
return $_A["__static_token"];
}
/**
* 设置tonken
* @param $token
*/
public static function setToken($token)
{
global $_A;
$_A["__static_token"] = $token;
}
/**
* 是否微信
* @return bool
@ -2147,6 +2044,27 @@ class Base
}
}
/**
* 获取平台类型
* @return string
*/
public static function platform()
{
$platform = strtolower(trim(Request::header('platform')));
if (in_array($platform, ['android', 'ios', 'win', 'mac', 'web'])) {
return $platform;
}
$agent = strtolower(Request::server('HTTP_USER_AGENT'));
if (str_contains($agent, 'android')) {
$platform = 'android';
} elseif (str_contains($agent, 'iphone') || str_contains($agent, 'ipad')) {
$platform = 'ios';
} else {
$platform = 'unknown';
}
return $platform;
}
/**
* 返回根据距离sql排序语句
* @param $lat
@ -2178,9 +2096,42 @@ class Base
return Min(Max(Base::nullShow(Request::input($inputName), $default), 1), $max);
}
/**
* base64语音保存
* @param array $param [ base64=带前缀的base64, path=>文件路径 ]
* @return array [name=>文件名, size=>文件大小(单位KB),file=>绝对地址, path=>相对地址, url=>全路径地址, ext=>文件后缀名]
*/
public static function record64save($param)
{
$base64 = $param['base64'];
if (preg_match('/^(data:\s*audio\/(\w+);base64,)/', $base64, $res)) {
$extension = $res[2];
if (!in_array($extension, ['mp3', 'wav'])) {
return Base::retError('语音格式错误');
}
$fileName = 'record_' . md5($base64) . '.' . $extension;
$fileDir = $param['path'];
$filePath = public_path($fileDir);
Base::makeDir($filePath);
if (file_put_contents($filePath . $fileName, base64_decode(str_replace($res[1], '', $base64)))) {
$fileSize = filesize($filePath . $fileName);
$array = [
"name" => $fileName, //原文件名
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
"file" => $filePath . $fileName, //文件的完整路径 "D:\www....KzZ.jpg"
"path" => $fileDir . $fileName, //相对路径 "uploads/pic....KzZ.jpg"
"url" => Base::fillUrl($fileDir . $fileName), //完整的URL "https://.....hhsKzZ.jpg"
"ext" => $extension, //文件后缀名
];
return Base::retSuccess('success', $array);
}
}
return Base::retError('语音保存失败');
}
/**
* image64图片保存
* @param array $param [ image64=带前缀的base64, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,, 压缩方式] ]
* @param array $param [ image64=带前缀的base64, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,, 压缩方式], autoThumb=>false不要自动生成缩略图 ]
* @return array [name=>文件名, size=>文件大小(单位KB),file=>绝对地址, path=>相对地址, url=>全路径地址, ext=>文件后缀名]
*/
public static function image64save($param)
@ -2200,7 +2151,7 @@ class Base
if ($width > 0 || $height > 0) {
$scaleName = "_{WIDTH}x{HEIGHT}";
if (isset($param['scale'][2])) {
$scaleName .= $param['scale'][2];
$scaleName .= "_c{$param['scale'][2]}";
}
}
}
@ -2261,8 +2212,13 @@ class Base
}
//生成缩略图
$array['thumb'] = $array['path'];
if (Base::imgThumb($array['file'], $array['file'] . "_thumb.jpg", 180, 0)) {
$array['thumb'] .= "_thumb.jpg";
if ($extension === 'gif' && !isset($param['autoThumb'])) {
$param['autoThumb'] = false;
}
if ($param['autoThumb'] !== false) {
if (Base::imgThumb($array['file'], $array['file'] . "_thumb.jpg", 320, 0)) {
$array['thumb'] .= "_thumb.jpg";
}
}
$array['thumb'] = Base::fillUrl($array['thumb']);
return Base::retSuccess('success', $array);
@ -2273,12 +2229,13 @@ class Base
/**
* 上传文件
* @param array $param [ type=[文件类型], file=>Request::file, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,, 压缩方式], size=>限制大小KB, autoThumb=>false不要自动生成缩略图 ]
* @param array $param [ type=[文件类型], file=>Request::file, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,, 压缩方式], size=>限制大小KB, autoThumb=>false不要自动生成缩略图, chmod=>权限(默认0644) ]
* @return array [name=>原文件名, size=>文件大小(单位KB),file=>绝对地址, path=>相对地址, url=>全路径地址, ext=>文件后缀名]
*/
public static function upload($param)
{
$file = $param['file'];
$chmod = $param['chmod'] ?: 0644;
if (empty($file)) {
return Base::retError("您没有选择要上传的文件");
}
@ -2323,30 +2280,7 @@ class Base
$type = ['yml', 'yaml', 'dmg', 'pkg', 'blockmap', 'zip', 'exe', 'msi'];
break;
case 'more':
$type = [
'text', 'md', 'markdown',
'drawio',
'mind',
'docx', 'wps', 'doc', 'xls', 'xlsx', 'ppt', 'pptx',
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'raw', 'svg',
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z', 'gz', 'apk', 'dmg',
'tif', 'tiff',
'dwg', 'dxf',
'ofd',
'pdf',
'txt',
'htaccess', 'htgroups', 'htpasswd', 'conf', 'bat', 'cmd', 'cpp', 'c', 'cc', 'cxx', 'h', 'hh', 'hpp', 'ino', 'cs', 'css',
'dockerfile', 'go', 'golang', 'html', 'htm', 'xhtml', 'vue', 'we', 'wpy', 'java', 'js', 'jsm', 'jsx', 'json', 'jsp', 'less', 'lua', 'makefile', 'gnumakefile',
'ocamlmakefile', 'make', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx',
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
'xmind',
'rp',
];
$type = []; // 不限制上传文件类型
break;
default:
return Base::retError('错误的类型参数');
@ -2360,7 +2294,7 @@ class Base
if ($param['size'] > 0 && $fileSize > $param['size'] * 1024) {
return Base::retError('文件大小超限,最大限制:' . $param['size'] . 'KB');
}
} catch (Exception $e) {
} catch (\Throwable) {
$fileSize = 0;
}
$scaleName = "";
@ -2374,30 +2308,36 @@ class Base
if ($width > 0 || $height > 0) {
$scaleName = "_{WIDTH}x{HEIGHT}";
if (isset($param['scale'][2])) {
$scaleName .= $param['scale'][2];
$scaleName .= "_c{$param['scale'][2]}";
}
}
}
$fileName = md5_file($file) . '.' . $extension;
$scaleName = md5_file($file) . $scaleName . '.' . $extension;
$fileName = md5_file($file);
$scaleName = md5_file($file) . $scaleName;
if ($extension) {
$fileName = $fileName . '.' . $extension;
$scaleName = $scaleName . '.' . $extension;
}
}
//
$file->move(public_path($param['path']), $fileName);
//
$path = $param['path'] . $fileName;
$array = [
"name" => $file->getClientOriginalName(), //原文件名
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
"file" => public_path($param['path'] . $fileName), //文件的完整路径 "D:\www....KzZ.jpg"
"path" => $param['path'] . $fileName, //相对路径 "uploads/pic....KzZ.jpg"
"url" => Base::fillUrl($param['path'] . $fileName), //完整的URL "https://.....hhsKzZ.jpg"
"thumb" => '', //缩略图(预览图) "https://.....hhsKzZ.jpg_thumb.jpg"
"width" => -1, //图片宽度
"height" => -1, //图片高度
"ext" => $extension, //文件后缀名
"name" => $file->getClientOriginalName(), //原文件名
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
"file" => public_path($path), //文件的完整路径 "D:\www....KzZ.jpg"
"path" => $path, //相对路径 "uploads/pic....KzZ.jpg"
"url" => Base::fillUrl($path), //完整的URL "https://.....hhsKzZ.jpg"
"thumb" => '', //缩略图(预览图) "https://.....hhsKzZ.jpg_thumb.jpg"
"width" => -1, //图片宽度
"height" => -1, //图片高度
"ext" => $extension, //文件后缀名
];
if (!is_file($array['file'])) {
return Base::retError('上传失败');
}
@chmod($array['file'], $chmod);
//iOS照片颠倒处理
if (in_array($extension, ['jpg', 'jpeg']) && function_exists('exif_read_data')) {
$data = imagecreatefromstring(file_get_contents($array['file']));
@ -2454,7 +2394,9 @@ class Base
}
//生成缩略图
$array['thumb'] = $array['path'];
if ($param['autoThumb'] === "false") $param['autoThumb'] = false;
if ($extension === 'gif' && !isset($param['autoThumb'])) {
$param['autoThumb'] = false;
}
if ($param['autoThumb'] !== false) {
if (Base::imgThumb($array['file'], $array['file'] . "_thumb.jpg", 320, 0)) {
$array['thumb'] .= "_thumb.jpg";
@ -2613,11 +2555,11 @@ class Base
}
$src = $createfun($src_img);
$dst = imagecreatetruecolor($width ? $width : $dst_w, $height ? $height : $dst_h);
$dst = imagecreatetruecolor($width ?: $dst_w, $height ?: $dst_h);
try {
$white = imagecolorallocate($dst, 255, 255, 255);
imagefill($dst, 0, 0, $white);
} catch (Exception $e) {
} catch (\Throwable) {
}
if (function_exists('imagecopyresampled')) {
@ -2802,36 +2744,34 @@ class Base
if (empty($str)) {
return '';
}
$fchar = ord($str[0]);
if ($fchar >= ord('A') && $fchar <= ord('z')) return strtoupper($str[0]);
$s1 = iconv('UTF-8', 'gb2312', $str);
$s2 = iconv('gb2312', 'UTF-8', $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 '#';
$first = mb_substr($str, 0, 1);
if (preg_match("/^\d$/", $first)) {
return '#';
}
if (!preg_match("/^[a-zA-Z]$/", $first)) {
$pinyin = new Pinyin();
$first = $pinyin->abbr($first, '', PINYIN_NAME);
}
return $first ? strtoupper($first) : '#';
}
/**
* 中文转拼音
* @param $str
* @return string
*/
public static function cn2pinyin($str)
{
if (empty($str)) {
return '';
}
if (!preg_match("/^[a-zA-Z0-9_.]+$/", $str)) {
$str = Cache::rememberForever("cn2pinyin:" . md5($str), function() use ($str) {
$pinyin = new Pinyin();
return $pinyin->permalink($str, '');
});
}
return $str;
}
/**
@ -3062,4 +3002,17 @@ class Base
throw new ApiException($validator->errors()->first());
}
}
/**
* 流下载,解决没有后缀无法下载的问题
* @param $callback
* @param $name
* @return mixed
*/
public static function streamDownload($callback, $name = null) {
if ($name && !str_contains($name, '.')) {
$name .= ".";
}
return Response::streamDownload($callback, $name);
}
}

View File

@ -14,11 +14,12 @@ use PhpOffice\PhpSpreadsheet\Writer\Exception;
class BillExport implements WithHeadings, WithEvents, FromCollection, WithTitle, WithStrictNullComparison
{
public $title;
public $headings = [];
public $data = [];
public $typeLists = [];
public $typeNumber = 0;
protected $title;
protected $headings = [];
protected $data = [];
protected $typeLists = [];
protected $typeNumber = 0;
protected $styles = [];
public function __construct($title, array $data)
{
@ -57,15 +58,19 @@ class BillExport implements WithHeadings, WithEvents, FromCollection, WithTitle,
return $this;
}
public function setStyles(array $styles)
{
$this->styles = $styles;
return $this;
}
public function store($fileName = '') {
if (empty($fileName)) {
$fileName = date("YmdHis") . '.xls';
}
try {
return Excel::store($this, $fileName);
} catch (Exception $e) {
return "导出错误:" . $e->getMessage();
} catch (\PhpOffice\PhpSpreadsheet\Exception $e) {
} catch (Exception|\PhpOffice\PhpSpreadsheet\Exception $e) {
return "导出错误:" . $e->getMessage();
}
}
@ -76,9 +81,7 @@ class BillExport implements WithHeadings, WithEvents, FromCollection, WithTitle,
}
try {
return Excel::download($this, $fileName);
} catch (Exception $e) {
return "导出错误:" . $e->getMessage();
} catch (\PhpOffice\PhpSpreadsheet\Exception $e) {
} catch (Exception|\PhpOffice\PhpSpreadsheet\Exception $e) {
return "导出错误:" . $e->getMessage();
}
}
@ -118,6 +121,11 @@ class BillExport implements WithHeadings, WithEvents, FromCollection, WithTitle,
{
return [
AfterSheet::Class => function (AfterSheet $event) {
if ($this->styles) {
foreach ($this->styles as $cell => $style) {
$event->sheet->getDelegate()->getStyle($cell)->applyFromArray($style);
}
}
$count = count($this->data);
foreach ($this->typeLists AS $cell => $typeList) {
if ($cell && $typeList) {

View File

@ -0,0 +1,24 @@
<?php
namespace App\Module;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class BillMultipleExport implements WithMultipleSheets
{
use Exportable;
protected $data = [];
public function __construct(array $data)
{
$this->data = $data;
}
public function sheets(): array
{
return $this->data;
}
}

302
app/Module/Doo.php Normal file
View File

@ -0,0 +1,302 @@
<?php
namespace App\Module;
use App\Exceptions\ApiException;
use App\Models\User;
use Carbon\Carbon;
use FFI;
class Doo
{
private static $doo;
/**
* char转为字符串
* @param $text
* @return string
*/
private static function string($text): string
{
return FFI::string($text);
}
/**
* 装载
* @param $token
* @param $language
*/
public static function load($token = null, $language = null)
{
self::$doo = FFI::cdef(<<<EOF
void initialize(char* work, char* token, char* lang);
char* license();
char* licenseDecode(char* license);
char* licenseSave(char* license);
int userId();
char* userExpiredAt();
char* userEmail();
char* userEncrypt();
char* userToken();
char* userCreate(char* email, char* password);
char* tokenEncode(int userid, char* email, char* encrypt, int days);
char* tokenDecode(char* val);
char* translate(char* val, char* val);
char* md5s(char* text, char* password);
char* macs();
char* dooSN();
EOF, "/usr/lib/doo/doo.so");
$token = $token ?: Base::headerOrInput('token');
$language = $language ?: Base::headerOrInput('language');
self::$doo->initialize("/var/www", $token, $language);
}
/**
* 获取实例
* @param $token
* @param $language
* @return mixed
*/
public static function doo($token = null, $language = null)
{
if (self::$doo == null) {
self::load($token, $language);
}
return self::$doo;
}
/**
* License
* @return array
*/
public static function license(): array
{
$array = Base::json2array(self::string(self::doo()->license()));
$ips = explode(",", $array['ip']);
$array['ip'] = [];
foreach ($ips as $ip) {
if (Base::is_ipv4($ip)) {
$array['ip'][] = $ip;
}
}
$domains = explode(",", $array['domain']);
$array['domain'] = [];
foreach ($domains as $domain) {
if (Base::is_domain($domain)) {
$array['domain'][] = $domain;
}
}
$macs = explode(",", $array['mac']);
$array['mac'] = [];
foreach ($macs as $mac) {
if (Base::isMac($mac)) {
$array['mac'][] = $mac;
}
}
$emails = explode(",", $array['email']);
$array['email'] = [];
foreach ($emails as $email) {
if (Base::isEmail($email)) {
$array['email'][] = $email;
}
}
return $array;
}
/**
* 获取License原文
* @return string
*/
public static function licenseContent(): string
{
if (env("SYSTEM_LICENSE") == 'hidden') {
return '';
}
$paths = [
config_path("LICENSE"),
config_path("license"),
app_path("LICENSE"),
app_path("license"),
];
$content = "";
foreach ($paths as $path) {
if (file_exists($path)) {
$content = file_get_contents($path);
break;
}
}
return $content;
}
/**
* 解析License
* @param $license
* @return array
*/
public static function licenseDecode($license): array
{
return Base::json2array(self::string(self::doo()->licenseDecode($license)));
}
/**
* 保存License
* @param $license
*/
public static function licenseSave($license): void
{
$res = self::string(self::doo()->licenseSave($license));
if ($res != 'success') {
throw new ApiException($res ?: 'LICENSE 保存失败');
}
}
/**
* 当前会员ID来自请求的token
* @return int
*/
public static function userId(): int
{
return intval(self::doo()->userId());
}
/**
* token是否过期来自请求的token
* @return bool
*/
public static function userExpired(): bool
{
$expiredAt = self::userExpiredAt();
return $expiredAt && Carbon::parse($expiredAt)->isBefore(Carbon::now());
}
/**
* token过期时间来自请求的token
* @return string
*/
public static function userExpiredAt(): string
{
$expiredAt = self::string(self::doo()->userExpiredAt());
return $expiredAt === 'forever' ? '' : $expiredAt;
}
/**
* 当前会员邮箱地址来自请求的token
* @return string
*/
public static function userEmail(): string
{
return self::string(self::doo()->userEmail());
}
/**
* 当前会员Encrypt来自请求的token
* @return string
*/
public static function userEncrypt(): string
{
return self::string(self::doo()->userEncrypt());
}
/**
* 当前会员token来自请求的token
* @return string
*/
public static function userToken(): string
{
return self::string(self::doo()->userToken());
}
/**
* 创建帐号
* @param $email
* @param $password
* @return User|null
*/
public static function userCreate($email, $password): User|null
{
$data = Base::json2array(self::string(self::doo()->userCreate($email, $password)));
if (Base::isError($data)) {
throw new ApiException($data['msg'] ?: '注册失败');
}
$user = User::whereEmail($email)->first();
if (empty($user)) {
throw new ApiException('注册失败');
}
return $user;
}
/**
* 生成token编码token
* @param $userid
* @param $email
* @param $encrypt
* @param int $days 有效时间(天)
* @return string
*/
public static function tokenEncode($userid, $email, $encrypt, int $days = 15): string
{
return self::string(self::doo()->tokenEncode($userid, $email, $encrypt, $days));
}
/**
* 解码token
* @param $token
* @return array
*/
public static function tokenDecode($token): array
{
return Base::json2array(self::string(self::doo()->tokenDecode($token)));
}
/**
* 翻译
* @param $text
* @param string $type
* @return string
*/
public static function translate($text, string $type = ""): string
{
return self::string(self::doo()->translate($text, $type));
}
/**
* md5防破解
* @param $text
* @param string $password
* @return string
*/
public static function md5s($text, string $password = ""): string
{
return self::string(self::doo()->md5s($text, $password));
}
/**
* 获取php容器mac地址组
* @return array
*/
public static function macs(): array
{
$macs = explode(",", self::string(self::doo()->macs()));
$array = [];
foreach ($macs as $mac) {
if (Base::isMac($mac)) {
$array[] = $mac;
}
}
return $array;
}
/**
* 获取当前SN
* @return string
*/
public static function dooSN(): string
{
return self::string(self::doo()->dooSN());
}
}

321
app/Module/Extranet.php Normal file
View File

@ -0,0 +1,321 @@
<?php
namespace App\Module;
use Cache;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
/**
* 外网资源请求
*/
class Extranet
{
/**
* 获取IP地址经纬度
* @param string $ip
* @return array
*/
public static function getIpGcj02(string $ip = ''): array
{
if (empty($ip)) {
$ip = Base::getIp();
}
$cacheKey = "getIpPoint::" . md5($ip);
$result = Cache::rememberForever($cacheKey, function () use ($ip) {
return Ihttp::ihttp_request("https://www.ifreesite.com/ipaddress/address.php?q=" . $ip, [], [], 12);
});
if (Base::isError($result)) {
Cache::forget($cacheKey);
return $result;
}
$data = $result['data'];
$lastPos = strrpos($data, ',');
$long = floatval(Base::getMiddle(substr($data, $lastPos + 1), null, ')'));
$lat = floatval(Base::getMiddle(substr($data, strrpos(substr($data, 0, $lastPos), ',') + 1), null, ','));
return Base::retSuccess("success", [
'long' => $long,
'lat' => $lat,
]);
}
/**
* 百度接口根据ip获取经纬度
* @param string $ip
* @return array
*/
public static function getIpGcj02ByBaidu(string $ip = ''): array
{
if (empty($ip)) {
$ip = Base::getIp();
}
$cacheKey = "getIpPoint::" . md5($ip);
$result = Cache::rememberForever($cacheKey, function () use ($ip) {
$ak = Config::get('app.baidu_app_key');
$url = 'http://api.map.baidu.com/location/ip?ak=' . $ak . '&ip=' . $ip . '&coor=bd09ll';
return Ihttp::ihttp_request($url, [], [], 12);
});
if (Base::isError($result)) {
Cache::forget($cacheKey);
return $result;
}
$data = json_decode($result['data'], true);
// x坐标纬度, y坐标经度
$long = Arr::get($data, 'content.point.x');
$lat = Arr::get($data, 'content.point.y');
return Base::retSuccess("success", [
'long' => $long,
'lat' => $lat,
]);
}
/**
* 获取IP地址详情
* @param string $ip
* @return array
*/
public static function getIpInfo(string $ip = ''): array
{
if (empty($ip)) {
$ip = Base::getIp();
}
$cacheKey = "getIpInfo::" . md5($ip);
$result = Cache::rememberForever($cacheKey, function () use ($ip) {
return Ihttp::ihttp_request("http://ip.taobao.com/service/getIpInfo.php?accessKey=alibaba-inc&ip=" . $ip, [], [], 12);
});
if (Base::isError($result)) {
Cache::forget($cacheKey);
return $result;
}
$data = json_decode($result['data'], true);
if (!is_array($data) || intval($data['code']) != 0) {
Cache::forget($cacheKey);
return Base::retError("error ip: -1");
}
$data = $data['data'];
if (!is_array($data) || !isset($data['country'])) {
return Base::retError("error ip: -2");
}
$data['text'] = $data['country'];
$data['textSmall'] = $data['country'];
if ($data['region'] && $data['region'] != $data['country'] && $data['region'] != "XX") {
$data['text'] .= " " . $data['region'];
$data['textSmall'] = $data['region'];
}
if ($data['city'] && $data['city'] != $data['region'] && $data['city'] != "XX") {
$data['text'] .= " " . $data['city'];
$data['textSmall'] .= " " . $data['city'];
}
if ($data['county'] && $data['county'] != $data['city'] && $data['county'] != "XX") {
$data['text'] .= " " . $data['county'];
$data['textSmall'] .= " " . $data['county'];
}
return Base::retSuccess("success", $data);
}
/**
* 判断是否工作日
* @param string $Ymd 年月日20220102
* @return int
* 0: 工作日
* 1: 非工作日
* 2: 获取不到远程数据的非工作日(周六、日)
* 所以可以用>0来判断是否工作日
*/
public static function isHoliday(string $Ymd): int
{
$time = strtotime($Ymd . " 00:00:00");
$holidayKey = "holiday::" . date("Ym", $time);
$holidayData = Cache::remember($holidayKey, now()->addMonth(), function () use ($time) {
$apiMonth = date("Ym", $time);
$apiResult = Ihttp::ihttp_request("https://api.apihubs.cn/holiday/get?field=date&month={$apiMonth}&workday=2&size=31", [], [], 20);
if (Base::isError($apiResult)) {
info('[holiday] get error');
return [];
}
$apiResult = Base::json2array($apiResult['data']);
if ($apiResult['code'] !== 0) {
info('[holiday] result error');
return [];
}
return array_map(function ($item) {
return $item['date'];
}, $apiResult['data']['list']);
});
if (empty($holidayData)) {
Cache::forget($holidayKey);
return in_array(date("w", $time), [0, 6]) ? 2 : 0;
}
return in_array($Ymd, $holidayData) ? 1 : 0;
}
/**
* Drawio 图标搜索
* @param $query
* @param $page
* @param $size
* @return array
*/
public static function drawioIconSearch($query, $page, $size): array
{
$result = self::curl("https://app.diagrams.net/iconSearch?q={$query}&p={$page}&c={$size}", 15 * 86400);
if ($result = Base::json2array($result)) {
return $result;
}
return [
'icons' => [],
'total_count' => 0
];
}
/**
* 随机笑话接口
* @return array
*/
public static function randJoke(): array
{
$jokeKey = env("JUKE_KEY_JOKE");
if ($jokeKey) {
$data = self::curl("http://v.juhe.cn/joke/randJoke.php?key=" . $jokeKey);
$data = Base::json2array($data);
if ($data['reason'] === 'success') {
return $data['result'];
}
}
return [];
}
/**
* 心灵鸡汤
* @return string
*/
public static function soups(): string
{
$soupKey = env("JUKE_KEY_SOUP");
if ($soupKey) {
$data = self::curl("https://apis.juhe.cn/fapig/soup/query?key=" . $soupKey);
$data = Base::json2array($data);
if ($data['reason'] === 'success' && $text = trim($data['result']['text'])) {
return $text;
}
}
return "";
}
/**
* 签到机器人网络内容
* @param $type
* @return string
*/
public static function checkinBotQuickMsg($type): string
{
$text = "维护中...";
switch ($type) {
case "it":
$data = self::curl('http://vvhan.api.hitosea.com/api/hotlist?type=itNews', 3600);
if ($data = Base::json2array($data)) {
$i = 1;
$array = array_map(function ($item) use (&$i) {
if ($item['title'] && $item['desc']) {
return "<p>" . ($i++) . ". <strong><a href='{$item['mobilUrl']}' target='_blank'>{$item['title']}</a></strong></p><p>{$item['desc']}</p>";
} else {
return null;
}
}, $data['data']);
$array = array_values(array_filter($array));
if ($array) {
array_unshift($array, "<p><strong>{$data['title']}</strong>{$data['update_time']}</p>");
$text = implode("<p>&nbsp;</p>", $array);
}
}
break;
case "36ke":
$data = self::curl('http://vvhan.api.hitosea.com/api/hotlist?type=36Ke', 3600);
if ($data = Base::json2array($data)) {
$i = 1;
$array = array_map(function ($item) use (&$i) {
if ($item['title'] && $item['desc']) {
return "<p>" . ($i++) . ". <strong><a href='{$item['mobilUrl']}' target='_blank'>{$item['title']}</a></strong></p><p>{$item['desc']}</p>";
} else {
return null;
}
}, $data['data']);
$array = array_values(array_filter($array));
if ($array) {
array_unshift($array, "<p><strong>{$data['title']}</strong>{$data['update_time']}</p>");
$text = implode("<p>&nbsp;</p>", $array);
}
}
break;
case "60s":
$data = self::curl('http://vvhan.api.hitosea.com/api/60s?type=json', 3600);
if ($data = Base::json2array($data)) {
$i = 1;
$array = array_map(function ($item) use (&$i) {
if ($item) {
return "<p>" . ($i++) . ". {$item}</p>";
} else {
return null;
}
}, $data['data']);
$array = array_values(array_filter($array));
if ($array) {
array_unshift($array, "<p><strong>{$data['name']}</strong>{$data['time'][0]}</p>");
$text = implode("<p>&nbsp;</p>", $array);
}
}
break;
case "joke":
$text = "笑话被掏空";
$data = self::curl('http://vvhan.api.hitosea.com/api/joke?type=json', 5);
if ($data = Base::json2array($data)) {
if ($data = trim($data['joke'])) {
$text = "开心笑话:{$data}";
}
}
break;
case "soup":
$text = "鸡汤分完了";
$data = self::curl('https://api.ayfre.com/jt/?type=bot', 5);
if ($data) {
$text = "心灵鸡汤:{$data}";
}
break;
}
return $text;
}
/**
* @param $url
* @param int $cacheSecond 缓存时间如果结果为空则缓存有效30秒
* @param int $timeout
* @return string
*/
private static function curl($url, int $cacheSecond = 0, int $timeout = 15): string
{
if ($cacheSecond > 0) {
$key = "curlCache::" . md5($url);
$content = Cache::remember($key, Carbon::now()->addSeconds($cacheSecond), function () use ($cacheSecond, $key, $timeout, $url) {
$result = Ihttp::ihttp_request($url, [], [], $timeout);
$content = Base::isSuccess($result) ? trim($result['data']) : '';
if (empty($content) && $cacheSecond > 30) {
Cache::put($key, "", Carbon::now()->addSeconds(30));
}
return $content;
});
} else {
$result = Ihttp::ihttp_request($url, [], [], $timeout);
$content = Base::isSuccess($result) ? trim($result['data']) : '';
}
//
return $content;
}
}

View File

@ -2,8 +2,6 @@
namespace App\Module;
use Exception;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class Ihttp
@ -88,7 +86,7 @@ class Ihttp
return Base::retError($error);
} else {
if ($isGb2312) {
try { $data = iconv('GB2312', 'UTF-8', $data); }catch (Exception $e) { }
try { $data = iconv('GB2312', 'UTF-8', $data); }catch (\Throwable) { }
}
$response = self::ihttp_response_parse($data);
Base::addLog([
@ -148,7 +146,7 @@ class Ihttp
$content .= fgets($fp, 512);
fclose($fp);
if ($isGb2312) {
try { $content = iconv('GB2312', 'UTF-8', $content); }catch (Exception $e) { }
try { $content = iconv('GB2312', 'UTF-8', $content); }catch (\Throwable) { }
}
$response = self::ihttp_response_parse($content, true);
Base::addLog([

390
app/Module/RandomColor.php Normal file
View File

@ -0,0 +1,390 @@
<?php
/**
* RandomColor 1.0.5
*
* PHP port of David Merfield JavaScript randomColor
* https://github.com/davidmerfield/randomColor
*
*
* The MIT License (MIT)
*
* Copyright (c) 2014-2022 Damien "Mistic" Sorel
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace App\Module;
class RandomColor
{
static public $dictionary = null;
private function __construct() {}
static public function one($options = array())
{
$h = self::_pickHue($options);
$s = self::_pickSaturation($h, $options);
$v = self::_pickBrightness($h, $s, $options);
return self::format(compact('h','s','v'), @$options['format']);
}
static public function many($count, $options = array())
{
$colors = array();
for ($i = 0; $i < $count; $i++)
{
$colors[] = self::one($options);
}
return $colors;
}
static public function format($hsv, $format='hex')
{
switch ($format)
{
case 'hsv':
return $hsv;
case 'hsl':
return self::hsv2hsl($hsv);
case 'hslCss':
$hsl = self::hsv2hsl($hsv);
return 'hsl(' . $hsl['h'] . ',' . $hsl['s'] . '%,' . $hsl['l'] . '%)';
case 'rgb':
return self::hsv2rgb($hsv);
case 'rgbCss':
return 'rgb(' . implode(',', self::hsv2rgb($hsv)) . ')';
case 'hex':
default:
return self::hsv2hex($hsv);
}
}
static private function _pickHue($options)
{
$range = self::_getHueRange($options);
if (empty($range))
{
return 0;
}
$hue = self::_rand($range, $options);
// Instead of storing red as two separate ranges,
// we group them, using negative numbers
if ($hue < 0)
{
$hue = 360 + $hue;
}
return $hue;
}
static private function _pickSaturation($h, $options)
{
if (@$options['hue'] === 'monochrome')
{
return 0;
}
if (@$options['luminosity'] === 'random')
{
return self::_rand(array(0, 100), $options);
}
$colorInfo = self::_getColorInfo($h);
$range = $colorInfo['s'];
switch (@$options['luminosity'])
{
case 'bright':
$range[0] = 55;
break;
case 'dark':
$range[0] = $range[1] - 10;
break;
case 'light':
$range[1] = 55;
break;
}
return self::_rand($range, $options);
}
static private function _pickBrightness($h, $s, $options)
{
if (@$options['luminosity'] === 'random')
{
$range = array(0, 100);
}
else
{
$range = array(
self::_getMinimumBrightness($h, $s),
100
);
switch (@$options['luminosity'])
{
case 'dark':
$range[1] = $range[0] + 20;
break;
case 'light':
$range[0] = round(($range[1] + $range[0]) / 2);
break;
}
}
return self::_rand($range, $options);
}
static private function _getHueRange($options)
{
$ranges = array();
if (isset($options['hue']))
{
if (!is_array($options['hue']))
{
$options['hue'] = array($options['hue']);
}
foreach ($options['hue'] as $hue)
{
if ($hue === 'random')
{
$ranges[] = array(0, 360);
}
else if (isset(self::$dictionary[$hue]))
{
$ranges[] = self::$dictionary[$hue]['h'];
}
else if (is_numeric($hue))
{
$hue = intval($hue);
if ($hue <= 360 && $hue >= 0)
{
$ranges[] = array($hue, $hue);
}
}
}
}
if (($l = count($ranges)) === 0)
{
return array(0, 360);
}
else if ($l === 1)
{
return $ranges[0];
}
else
{
return $ranges[self::_rand(array(0, $l-1), $options)];
}
}
static private function _getMinimumBrightness($h, $s)
{
$colorInfo = self::_getColorInfo($h);
$bounds = $colorInfo['bounds'];
for ($i = 0, $l = count($bounds); $i < $l - 1; $i++)
{
$s1 = $bounds[$i][0];
$v1 = $bounds[$i][1];
$s2 = $bounds[$i+1][0];
$v2 = $bounds[$i+1][1];
if ($s >= $s1 && $s <= $s2)
{
$m = ($v2 - $v1) / ($s2 - $s1);
$b = $v1 - $m * $s1;
return round($m * $s + $b);
}
}
return 0;
}
static private function _getColorInfo($h)
{
// Maps red colors to make picking hue easier
if ($h >= 334 && $h <= 360)
{
$h-= 360;
}
foreach (self::$dictionary as $color)
{
if ($color['h'] !== null && $h >= $color['h'][0] && $h <= $color['h'][1])
{
return $color;
}
}
}
static private function _rand($bounds, $options)
{
if (isset($options['prng']))
{
return $options['prng']($bounds[0], $bounds[1]);
}
else
{
return mt_rand($bounds[0], $bounds[1]);
}
}
static public function hsv2hex($hsv)
{
$rgb = self::hsv2rgb($hsv);
$hex = '#';
foreach ($rgb as $c)
{
$hex.= str_pad(dechex($c), 2, '0', STR_PAD_LEFT);
}
return $hex;
}
static public function hsv2hsl($hsv)
{
extract($hsv);
$s/= 100;
$v/= 100;
$k = (2-$s)*$v;
return array(
'h' => $h,
's' => round($s*$v / ($k < 1 ? $k : 2-$k), 4) * 100,
'l' => $k/2 * 100,
);
}
static public function hsv2rgb($hsv)
{
extract($hsv);
$h/= 360;
$s/= 100;
$v/= 100;
$i = floor($h * 6);
$f = $h * 6 - $i;
$m = $v * (1 - $s);
$n = $v * (1 - $s * $f);
$k = $v * (1 - $s * (1 - $f));
$r = 1;
$g = 1;
$b = 1;
switch ($i)
{
case 0:
list($r,$g,$b) = array($v,$k,$m);
break;
case 1:
list($r,$g,$b) = array($n,$v,$m);
break;
case 2:
list($r,$g,$b) = array($m,$v,$k);
break;
case 3:
list($r,$g,$b) = array($m,$n,$v);
break;
case 4:
list($r,$g,$b) = array($k,$m,$v);
break;
case 5:
case 6:
list($r,$g,$b) = array($v,$m,$n);
break;
}
return array(
'r' => floor($r*255),
'g' => floor($g*255),
'b' => floor($b*255),
);
}
}
/*
* h=hueRange
* s=saturationRange : bounds[0][0] ; bounds[-][0]
*/
RandomColor::$dictionary = array(
'monochrome' => array(
'bounds' => array(array(0,0), array(100,0)),
'h' => NULL,
's' => array(0,100)
),
'red' => array(
'bounds' => array(array(20,100),array(30,92),array(40,89),array(50,85),array(60,78),array(70,70),array(80,60),array(90,55),array(100,50)),
'h' => array(-26,18),
's' => array(20,100)
),
'orange' => array(
'bounds' => array(array(20,100),array(30,93),array(40,88),array(50,86),array(60,85),array(70,70),array(100,70)),
'h' => array(19,46),
's' => array(20,100)
),
'yellow' => array(
'bounds' => array(array(25,100),array(40,94),array(50,89),array(60,86),array(70,84),array(80,82),array(90,80),array(100,75)),
'h' => array(47,62),
's' => array(25,100)
),
'green' => array(
'bounds' => array(array(30,100),array(40,90),array(50,85),array(60,81),array(70,74),array(80,64),array(90,50),array(100,40)),
'h' => array(63,178),
's' => array(30,100)
),
'blue' => array(
'bounds' => array(array(20,100),array(30,86),array(40,80),array(50,74),array(60,60),array(70,52),array(80,44),array(90,39),array(100,35)),
'h' => array(179,257),
's' => array(20,100)
),
'purple' => array(
'bounds' => array(array(20,100),array(30,87),array(40,79),array(50,70),array(60,65),array(70,59),array(80,52),array(90,45),array(100,42)),
'h' => array(258,282),
's' => array(20,100)
),
'pink' => array(
'bounds' => array(array(20,100),array(30,90),array(40,86),array(60,84),array(80,80),array(90,75),array(100,73)),
'h' => array(283,334),
's' => array(20,100)
)
);

44
app/Module/TimeRange.php Normal file
View File

@ -0,0 +1,44 @@
<?php
namespace App\Module;
use Carbon\Carbon;
class TimeRange
{
public ?Carbon $updated;
public ?Carbon $deleted;
/**
* @param $data
*/
public function __construct($data)
{
if (is_array($data)) {
$range = explode("-", str_replace([",", "|"], "-", $data['timerange']));
if ($data['updated_at'] || $data['at_after']) {
$range[0] = $data['updated_at'] ?: $data['at_after'];
}
if ($data['deleted_at']) {
$range[1] = $data['deleted_at'];
}
} else {
$range = explode("-", str_replace([",", "|"], "-", $data));
}
//
$updated = Base::isNumber($range[0]) ? intval($range[0]) : trim($range[0]);
$deleted = Base::isNumber($range[1]) ? intval($range[1]) : trim($range[1]);
//
$this->updated = $updated ? Carbon::parse($updated) : null;
$this->deleted = $deleted ? Carbon::parse($deleted) : null;
}
/**
* @param $data
* @return TimeRange
*/
public static function parse($data): TimeRange
{
return new self($data);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Observers;
use App\Models\Deleted;
use App\Models\Project;
use App\Models\ProjectUser;
class ProjectObserver
{
/**
* Handle the Project "created" event.
*
* @param \App\Models\Project $project
* @return void
*/
public function created(Project $project)
{
//
}
/**
* Handle the Project "updated" event.
*
* @param \App\Models\Project $project
* @return void
*/
public function updated(Project $project)
{
if ($project->isDirty('archived_at')) {
$userids = $this->userids($project);
if ($project->archived_at) {
Deleted::record('project', $project->id, $userids);
} else {
Deleted::forget('project', $project->id, $userids);
}
}
}
/**
* Handle the Project "deleted" event.
*
* @param \App\Models\Project $project
* @return void
*/
public function deleted(Project $project)
{
Deleted::record('project', $project->id, $this->userids($project));
}
/**
* Handle the Project "restored" event.
*
* @param \App\Models\Project $project
* @return void
*/
public function restored(Project $project)
{
Deleted::forget('project', $project->id, $this->userids($project));
}
/**
* Handle the Project "force deleted" event.
*
* @param \App\Models\Project $project
* @return void
*/
public function forceDeleted(Project $project)
{
//
}
/**
* @param Project $project
* @return array
*/
private function userids(Project $project)
{
return ProjectUser::whereProjectId($project->id)->pluck('userid')->toArray();
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Observers;
use App\Models\Deleted;
use App\Models\ProjectTask;
use App\Models\ProjectTaskUser;
class ProjectTaskObserver
{
/**
* Handle the ProjectTask "created" event.
*
* @param \App\Models\ProjectTask $projectTask
* @return void
*/
public function created(ProjectTask $projectTask)
{
//
}
/**
* Handle the ProjectTask "updated" event.
*
* @param \App\Models\ProjectTask $projectTask
* @return void
*/
public function updated(ProjectTask $projectTask)
{
if ($projectTask->isDirty('archived_at')) {
$userids = $this->userids($projectTask);
if ($projectTask->archived_at) {
Deleted::record('projectTask', $projectTask->id, $userids);
} else {
Deleted::forget('projectTask', $projectTask->id, $userids);
}
}
}
/**
* Handle the ProjectTask "deleted" event.
*
* @param \App\Models\ProjectTask $projectTask
* @return void
*/
public function deleted(ProjectTask $projectTask)
{
Deleted::record('projectTask', $projectTask->id, $this->userids($projectTask));
}
/**
* Handle the ProjectTask "restored" event.
*
* @param \App\Models\ProjectTask $projectTask
* @return void
*/
public function restored(ProjectTask $projectTask)
{
Deleted::forget('projectTask', $projectTask->id, $this->userids($projectTask));
}
/**
* Handle the ProjectTask "force deleted" event.
*
* @param \App\Models\ProjectTask $projectTask
* @return void
*/
public function forceDeleted(ProjectTask $projectTask)
{
//
}
/**
* @param ProjectTask $projectTask
* @return array
*/
private function userids(ProjectTask $projectTask)
{
return ProjectTaskUser::whereTaskId($projectTask->id)->pluck('userid')->toArray();
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Observers;
use App\Models\Deleted;
use App\Models\ProjectTaskUser;
use App\Models\ProjectUser;
class ProjectTaskUserObserver
{
/**
* Handle the ProjectTaskUser "created" event.
*
* @param \App\Models\ProjectTaskUser $projectTaskUser
* @return void
*/
public function created(ProjectTaskUser $projectTaskUser)
{
Deleted::forget('projectTask', $projectTaskUser->task_id, $projectTaskUser->userid);
}
/**
* Handle the ProjectTaskUser "updated" event.
*
* @param \App\Models\ProjectTaskUser $projectTaskUser
* @return void
*/
public function updated(ProjectTaskUser $projectTaskUser)
{
//
}
/**
* Handle the ProjectTaskUser "deleted" event.
*
* @param \App\Models\ProjectTaskUser $projectTaskUser
* @return void
*/
public function deleted(ProjectTaskUser $projectTaskUser)
{
if (!ProjectUser::whereProjectId($projectTaskUser->project_id)->whereUserid($projectTaskUser->userid)->exists()) {
Deleted::record('projectTask', $projectTaskUser->task_id, $projectTaskUser->userid);
}
}
/**
* Handle the ProjectTaskUser "restored" event.
*
* @param \App\Models\ProjectTaskUser $projectTaskUser
* @return void
*/
public function restored(ProjectTaskUser $projectTaskUser)
{
//
}
/**
* Handle the ProjectTaskUser "force deleted" event.
*
* @param \App\Models\ProjectTaskUser $projectTaskUser
* @return void
*/
public function forceDeleted(ProjectTaskUser $projectTaskUser)
{
//
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Observers;
use App\Models\Deleted;
use App\Models\ProjectUser;
class ProjectUserObserver
{
/**
* Handle the ProjectUser "created" event.
*
* @param \App\Models\ProjectUser $projectUser
* @return void
*/
public function created(ProjectUser $projectUser)
{
Deleted::forget('project', $projectUser->project_id, $projectUser->userid);
}
/**
* Handle the ProjectUser "updated" event.
*
* @param \App\Models\ProjectUser $projectUser
* @return void
*/
public function updated(ProjectUser $projectUser)
{
//
}
/**
* Handle the ProjectUser "deleted" event.
*
* @param \App\Models\ProjectUser $projectUser
* @return void
*/
public function deleted(ProjectUser $projectUser)
{
Deleted::record('project', $projectUser->project_id, $projectUser->userid);
}
/**
* Handle the ProjectUser "restored" event.
*
* @param \App\Models\ProjectUser $projectUser
* @return void
*/
public function restored(ProjectUser $projectUser)
{
//
}
/**
* Handle the ProjectUser "force deleted" event.
*
* @param \App\Models\ProjectUser $projectUser
* @return void
*/
public function forceDeleted(ProjectUser $projectUser)
{
//
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Observers;
use App\Models\Deleted;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogUser;
class WebSocketDialogObserver
{
/**
* Handle the WebSocketDialog "created" event.
*
* @param \App\Models\WebSocketDialog $webSocketDialog
* @return void
*/
public function created(WebSocketDialog $webSocketDialog)
{
//
}
/**
* Handle the WebSocketDialog "updated" event.
*
* @param \App\Models\WebSocketDialog $webSocketDialog
* @return void
*/
public function updated(WebSocketDialog $webSocketDialog)
{
//
}
/**
* Handle the WebSocketDialog "deleted" event.
*
* @param \App\Models\WebSocketDialog $webSocketDialog
* @return void
*/
public function deleted(WebSocketDialog $webSocketDialog)
{
Deleted::record('dialog', $webSocketDialog->id, $this->userids($webSocketDialog));
}
/**
* Handle the WebSocketDialog "restored" event.
*
* @param \App\Models\WebSocketDialog $webSocketDialog
* @return void
*/
public function restored(WebSocketDialog $webSocketDialog)
{
Deleted::forget('dialog', $webSocketDialog->id, $this->userids($webSocketDialog));
}
/**
* Handle the WebSocketDialog "force deleted" event.
*
* @param \App\Models\WebSocketDialog $webSocketDialog
* @return void
*/
public function forceDeleted(WebSocketDialog $webSocketDialog)
{
//
}
/**
* @param WebSocketDialog $webSocketDialog
* @return array
*/
private function userids(WebSocketDialog $webSocketDialog)
{
return WebSocketDialogUser::whereDialogId($webSocketDialog->id)->pluck('userid')->toArray();
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Observers;
use App\Models\Deleted;
use App\Models\WebSocketDialogUser;
class WebSocketDialogUserObserver
{
/**
* Handle the WebSocketDialogUser "created" event.
*
* @param \App\Models\WebSocketDialogUser $webSocketDialogUser
* @return void
*/
public function created(WebSocketDialogUser $webSocketDialogUser)
{
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
}
/**
* Handle the WebSocketDialogUser "updated" event.
*
* @param \App\Models\WebSocketDialogUser $webSocketDialogUser
* @return void
*/
public function updated(WebSocketDialogUser $webSocketDialogUser)
{
//
}
/**
* Handle the WebSocketDialogUser "deleted" event.
*
* @param \App\Models\WebSocketDialogUser $webSocketDialogUser
* @return void
*/
public function deleted(WebSocketDialogUser $webSocketDialogUser)
{
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
}
/**
* Handle the WebSocketDialogUser "restored" event.
*
* @param \App\Models\WebSocketDialogUser $webSocketDialogUser
* @return void
*/
public function restored(WebSocketDialogUser $webSocketDialogUser)
{
//
}
/**
* Handle the WebSocketDialogUser "force deleted" event.
*
* @param \App\Models\WebSocketDialogUser $webSocketDialogUser
* @return void
*/
public function forceDeleted(WebSocketDialogUser $webSocketDialogUser)
{
//
}
}

View File

@ -2,10 +2,21 @@
namespace App\Providers;
use App\Models\Project;
use App\Models\ProjectTask;
use App\Models\ProjectTaskUser;
use App\Models\ProjectUser;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogUser;
use App\Observers\ProjectObserver;
use App\Observers\ProjectTaskObserver;
use App\Observers\ProjectTaskUserObserver;
use App\Observers\ProjectUserObserver;
use App\Observers\WebSocketDialogObserver;
use App\Observers\WebSocketDialogUserObserver;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
@ -27,6 +38,11 @@ class EventServiceProvider extends ServiceProvider
*/
public function boot()
{
//
Project::observe(ProjectObserver::class);
ProjectTask::observe(ProjectTaskObserver::class);
ProjectTaskUser::observe(ProjectTaskUserObserver::class);
ProjectUser::observe(ProjectUserObserver::class);
WebSocketDialog::observe(WebSocketDialogObserver::class);
WebSocketDialogUser::observe(WebSocketDialogUserObserver::class);
}
}

View File

@ -8,6 +8,7 @@ use App\Models\User;
use App\Models\WebSocket;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Doo;
use App\Tasks\LineTask;
use App\Tasks\PushTask;
use Cache;
@ -39,55 +40,44 @@ class WebSocketService implements WebSocketHandlerInterface
*/
public function onOpen(Server $server, Request $request)
{
global $_A;
$_A = [
'__static_langdata' => [],
];
$fd = $request->fd;
$data = Base::newTrim($request->get);
$action = $data['action'];
$get = Base::newTrim($request->get);
$action = $get['action'];
switch ($action) {
/**
* 网页访问
*/
case 'web':
{
// 判断token参数
$token = $data['token'];
$cacheKey = "ws::token:" . md5($token);
$userid = Cache::remember($cacheKey, now()->addSeconds(1), function () use ($token) {
$authInfo = User::authFind('all', $token);
if ($authInfo['userid'] > 0) {
if (User::whereUserid($authInfo['userid'])->whereEmail($authInfo['email'])->whereEncrypt($authInfo['encrypt'])->exists()) {
return $authInfo['userid'];
}
}
return 0;
});
if (empty($userid)) {
Cache::forget($cacheKey);
Doo::load($get['token'], $get['language']);
//
if (Doo::userId() > 0
&& !Doo::userExpired()
&& $user = User::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first()) {
// 保存用户
$this->saveUser($fd, $user->userid);
// 发送open事件
$server->push($fd, Base::array2json([
'type' => 'open',
'data' => [
'fd' => $fd,
],
]));
// 通知上线
Task::deliver(new LineTask($user->userid, true));
// 推送离线时收到的消息
Task::deliver(new PushTask("RETRY::" . $user->userid));
} else {
// 用户不存在
$server->push($fd, Base::array2json([
'type' => 'error',
'data' => [
'error' => '会员不存在'
'error' => 'No member'
],
]));
$server->close($fd);
$this->deleteUser($fd);
return;
}
// 保存用户、发送open事件
$this->saveUser($fd, $userid);
$server->push($fd, Base::array2json([
'type' => 'open',
'data' => [
'fd' => $fd,
],
]));
// 通知上线
Task::deliver(new LineTask($userid, true));
// 推送离线时收到的消息
Task::deliver(new PushTask("RETRY::" . $userid));
}
break;
@ -103,11 +93,6 @@ class WebSocketService implements WebSocketHandlerInterface
*/
public function onMessage(Server $server, Frame $frame)
{
global $_A;
$_A = [
'__static_langdata' => [],
];
//
$msg = Base::json2array($frame->data);
$type = $msg['type']; // 消息类型
$msgId = $msg['msgId']; // 消息ID用于回调
@ -145,10 +130,10 @@ class WebSocketService implements WebSocketHandlerInterface
$pathOld = $row->path;
$row->path = $pathNew;
$row->save();
if (preg_match("/^file\/content\/\d+$/", $pathOld)) {
if (preg_match("/^\/single\/file\/\d+$/", $pathOld)) {
$this->pushPath($pathOld);
}
if (preg_match("/^file\/content\/\d+$/", $pathNew)) {
if (preg_match("/^\/single\/file\/\d+$/", $pathNew)) {
$this->pushPath($pathNew);
}
}
@ -191,13 +176,15 @@ class WebSocketService implements WebSocketHandlerInterface
*/
private function saveUser($fd, $userid)
{
Cache::put("User::fd:" . $fd, "on", Carbon::now()->addDay());
Cache::put("User::online:" . $userid, "on", Carbon::now()->addDay());
//
WebSocket::updateInsert([
'key' => md5($fd . '@' . $userid)
], [
'fd' => $fd,
'userid' => $userid,
]);
Cache::put("User::online:" . $userid, time(), Carbon::now()->addSeconds(30));
}
/**
@ -206,12 +193,21 @@ class WebSocketService implements WebSocketHandlerInterface
*/
private function deleteUser($fd)
{
Cache::forget("User::fd:" . $fd);
//
$array = [];
WebSocket::whereFd($fd)->chunk(10, function($list) use (&$array) {
/** @var WebSocket $item */
foreach ($list as $item) {
$item->delete();
if ($item->path && str_starts_with($item->path, "file/content/")) {
if ($item->userid) {
// 离线时更新会员最后在线时间
User::whereUserid($item->userid)->update([
'line_at' => Carbon::now()
]);
Cache::forget("User::online:" . $item->userid);
}
if ($item->path && str_starts_with($item->path, "/single/file/")) {
$array[$item->path] = $item->path;
}
}

View File

@ -1,6 +1,9 @@
<?php
namespace App\Tasks;
use App\Models\TaskWorker;
use App\Module\Base;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
/**
@ -9,27 +12,18 @@ use Hhxsv5\LaravelS\Swoole\Task\Task;
*/
abstract class AbstractTask extends Task
{
protected $newTask = [];
protected int $twid = 0;
/**
* 添加完成后执行的任务
* @param $task
*/
final protected function addTask($task)
public function __construct(...$params)
{
$this->newTask[] = $task;
}
/**
* 包装执行过程
*/
final public function handle()
{
try {
$this->start();
} catch (\Exception $e) {
$this->info($e);
$this->failed($e);
$row = TaskWorker::createInstance([
'args' => [
'params' => $params,
'class' => get_class($this)
],
]);
if ($row->save()) {
$this->twid = $row->id;
}
}
@ -41,31 +35,53 @@ abstract class AbstractTask extends Task
/**
* 任务完成事件
*/
public function finish()
abstract public function end();
/**
* 重写执行过程
*/
final public function handle()
{
foreach ($this->newTask AS $task) {
Task::deliver($task);
TaskWorker::whereId($this->twid)->update(['start_at' => Carbon::now()]);
//
try {
$this->start();
} catch (\Throwable $e) {
$this->failed("start", $e);
}
}
/**
* 重写完成事件
*/
final public function finish()
{
TaskWorker::whereId($this->twid)->update(['end_at' => Carbon::now()]);
//
try {
$this->end();
TaskWorker::whereId($this->twid)->delete();
} catch (\Throwable $e) {
$this->failed("end", $e);
}
}
/**
* 任务失败事件
* @param $e
* @param string $type
* @param \Throwable $e
*/
public function failed($e)
public function failed(string $type, \Throwable $e)
{
info($type);
info($e);
//
}
/**
* 添加日志
* @param $var
*/
private function info($var)
{
if (!config('app.debug') || defined('DO_NOT_ADD_LOGS')) {
return;
}
info($var);
TaskWorker::whereId($this->twid)->update(['error' => Base::array2json([
'time' => Carbon::now(),
'type' => $type,
'code' => $e->getCode(),
'file' => $e->getFile(),
'message' => $e->getMessage(),
])]);
}
}

52
app/Tasks/AppPushTask.php Normal file
View File

@ -0,0 +1,52 @@
<?php
namespace App\Tasks;
use App\Models\ProjectTask;
use App\Module\Base;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class AppPushTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
public function start()
{
$setting = Base::setting('appPushSetting');
if ($setting['push'] !== 'open') {
return;
}
ProjectTask::whereNull("complete_at")
->whereNull("archived_at")
->whereBetween("end_at", [
Carbon::now()->addMinutes(60),
Carbon::now()->addMinutes(60 + 10)
])->chunkById(100, function ($tasks) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$task->taskPush(null, 1);
}
});
ProjectTask::whereNull("complete_at")
->whereNull("archived_at")
->whereBetween("end_at", [
Carbon::now()->subMinutes(60 + 10),
Carbon::now()->subMinutes(60)
])->chunkById(100, function ($tasks) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$task->taskPush(null, 2);
}
});
}
public function end()
{
}
}

View File

@ -18,7 +18,7 @@ class AutoArchivedTask extends AbstractTask
public function __construct()
{
//
parent::__construct();
}
public function start()
@ -42,4 +42,9 @@ class AutoArchivedTask extends AbstractTask
}
}
}
public function end()
{
}
}

View File

@ -0,0 +1,386 @@
<?php
namespace App\Tasks;
use App\Models\User;
use App\Models\UserBot;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Ihttp;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 推送会话消息
* Class BotReceiveMsgTask
* @package App\Tasks
*/
class BotReceiveMsgTask extends AbstractTask
{
protected $userid;
protected $msgId;
public function __construct($userid, $msgId)
{
parent::__construct(...func_get_args());
$this->userid = $userid;
$this->msgId = $msgId;
}
public function start()
{
$botUser = User::whereUserid($this->userid)->whereBot(1)->first();
if (empty($botUser)) {
return;
}
$msg = WebSocketDialogMsg::find($this->msgId);
if (empty($msg)) {
return;
}
$msg->readSuccess($botUser->userid);
//
$dialog = WebSocketDialog::find($msg->dialog_id);
if (empty($dialog)) {
return;
}
if ($dialog->type !== 'user') {
return;
}
$this->botManagerReceive($msg, $botUser);
}
public function end()
{
}
/**
* 机器人管理处理消息
* @param WebSocketDialogMsg $msg
* @param User $botUser
* @return void
*/
private function botManagerReceive(WebSocketDialogMsg $msg, User $botUser)
{
if ($msg->type !== 'text') {
return;
}
$original = $msg->msg['text'];
if (preg_match("/<span[^>]*?data-quick-key=([\"'])(.*?)\\1[^>]*?>(.*?)<\/span>/is", $original, $match)) {
$command = $match[2];
} else {
$command = trim(strip_tags($original));
}
// 签到机器人
if ($botUser->email === 'check-in@bot.system') {
$text = UserBot::checkinBotQuickMsg($command, $msg->userid);
if ($text) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
}
}
// 隐私机器人
if ($botUser->email === 'anon-msg@bot.system') {
$text = UserBot::anonBotQuickMsg($command);
if ($text) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
}
}
// 管理机器人
if (str_starts_with($command, '/')) {
if ($botUser->email === 'bot-manager@bot.system') {
$isManager = true;
} elseif (UserBot::whereBotId($botUser->userid)->whereUserid($msg->userid)->exists()) {
$isManager = false;
} else {
$text = "非常抱歉,我不是你的机器人,无法完成你的指令。";
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
return;
}
//
$array = Base::newTrim(explode(" ", "{$command} "));
$type = $array[0];
$data = [];
$notice = "";
if (!$isManager && in_array($type, ['/list', '/newbot'])) {
return; // 这些操作仅支持【机器人管理】机器人
}
switch ($type) {
/**
* 列表
*/
case '/list':
$data = User::select([
'users.*',
'user_bots.clear_day',
'user_bots.clear_at',
'user_bots.webhook_url',
'user_bots.webhook_num'
])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.userid', $msg->userid)
->take(50)
->orderByDesc('id')
->get();
if ($data->isEmpty()) {
$type = "notice";
$notice = "您没有创建机器人。";
}
break;
/**
* 详情
*/
case '/info':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if (!$data) {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 创建
*/
case '/newbot':
if (User::select(['users.*'])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.userid', $msg->userid)
->count() >= 50) {
$type = "notice";
$notice = "超过最大创建数量。";
break;
}
if (strlen($array[1]) < 2 || strlen($array[1]) > 20) {
$type = "notice";
$notice = "机器人名称由2-20个字符组成。";
break;
}
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
'nickname' => $array[1]
], $msg->userid);
if (empty($data)) {
$type = "notice";
$notice = "创建失败。";
break;
}
$dialog = WebSocketDialog::checkUserDialog($data, $msg->userid);
if ($dialog) {
$text = "<p>您好,我是机器人:{$data->nickname}我的机器人ID是{$data->userid}</p><p>你可以发送 <u><b>/help</b></u> 查看我支持什么命令。</p>";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $data->userid); // todo 未能在任务end事件来发送任务
}
break;
/**
* 修改名字
*/
case '/setname':
$botId = $isManager ? $array[1] : $botUser->userid;
$nameString = $isManager ? $array[2] : $array[1];
if (strlen($nameString) < 2 || strlen($nameString) > 20) {
$type = "notice";
$notice = "机器人名称由2-20个字符组成。";
break;
}
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$data->nickname = $nameString;
$data->az = Base::getFirstCharter($nameString);
$data->pinyin = Base::cn2pinyin($nameString);
$data->save();
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 删除
*/
case '/deletebot':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$data->deleteUser('delete bot');
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 获取Token
*/
case '/token':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
User::generateToken($data);
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 更新Token
*/
case '/revoke':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$data->encrypt = Base::generatePassword(6);
$data->password = Doo::md5s(Base::generatePassword(32), $data->encrypt);
$data->save();
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 设置自动清理消息时间
*/
case '/clearday':
$botId = $isManager ? $array[1] : $botUser->userid;
$clearDay = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$userBot = UserBot::whereBotId($botId)->whereUserid($msg->userid)->first();
if ($userBot) {
$userBot->clear_day = min(intval($clearDay) ?: 30, 999);
$userBot->clear_at = Carbon::now()->addDays($userBot->clear_day);
$userBot->save();
}
$data->clear_day = $userBot->clear_day;
$data->clear_at = $userBot->clear_at; // 这两个参数只是作为输出,所以不保存
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 设置webhook
*/
case '/webhook':
$botId = $isManager ? $array[1] : $botUser->userid;
$webhookUrl = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
if (strlen($webhookUrl) > 255) {
$type = "notice";
$notice = "webhook地址最长仅支持255个字符。";
} elseif ($data) {
$userBot = UserBot::whereBotId($botId)->whereUserid($msg->userid)->first();
if ($userBot) {
$userBot->webhook_url = $webhookUrl ?: "";
$userBot->webhook_num = 0;
$userBot->save();
}
$data->webhook_url = $userBot->webhook_url ?: '-';
$data->webhook_num = $userBot->webhook_num; // 这两个参数只是作为输出,所以不保存
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 会话搜索
*/
case '/dialog':
$botId = $isManager ? $array[1] : $botUser->userid;
$nameKey = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.name', 'LIKE', "%{$nameKey}%")
->where('u.userid', $data->userid)
->orderByDesc('u.top_at')
->orderByDesc('web_socket_dialogs.last_at')
->take(20)
->get();
if ($list->isEmpty()) {
$type = "notice";
$notice = "没有搜索到相关会话。";
} else {
$list->transform(function (WebSocketDialog $item) use ($data) {
return $item->formatData($data->userid);
});
$data->list = $list; // 这个参数只是作为输出,所以不保存
}
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
}
//
$text = view('push.bot', [
'type' => $type,
'data' => $data,
'notice' => $notice,
'manager' => $isManager,
'version' => Base::getVersion()
])->render();
if (!$isManager) {
$text = preg_replace("/\s*\{机器人ID\}/", "", $text);
}
$text = preg_replace("/^\x20+/", "", $text);
$text = preg_replace("/\n\x20+/", "\n", $text);
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
return;
}
// 推送Webhook
if ($command) {
$userBot = UserBot::whereBotId($botUser->userid)->first();
if ($userBot && preg_match("/^https*:\/\//", $userBot->webhook_url)) {
Ihttp::ihttp_post($userBot->webhook_url, [
'text' => $command,
'token' => User::generateToken($botUser),
'dialog_id' => $msg->dialog_id,
'msg_id' => $msg->id,
'msg_uid' => $msg->userid,
'bot_uid' => $botUser->userid,
'version' => Base::getVersion(),
], 10);
$userBot->webhook_num++;
$userBot->save();
}
}
}
/**
* @param $botId
* @param $userid
* @return User
*/
private function botManagerOne($botId, $userid)
{
$botId = intval($botId);
$userid = intval($userid);
if ($botId > 0) {
return User::select([
'users.*',
'user_bots.clear_day',
'user_bots.clear_at',
'user_bots.webhook_url',
'user_bots.webhook_num'
])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.bot_id', $botId)
->where('user_bots.userid', $userid)
->first();
}
return null;
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Tasks;
use App\Models\User;
use App\Models\UserCheckinRecord;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Extranet;
use Cache;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class CheckinRemindTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
public function start()
{
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return;
}
// 判断非工作日
if (Extranet::isHoliday(date("Ymd")) > 0) {
return;
}
//
$times = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00'];
$remindin = (intval($setting['remindin']) ?: 5) * 60;
$remindexceed = (intval($setting['remindexceed']) ?: 10) * 60;
//
$nowDate = date("Y-m-d");
$timeStart = strtotime("{$nowDate} {$times[0]}");
//
if ($remindin > 0) {
$timeRemindin = $timeStart - $remindin;
if ($timeRemindin <= Base::time() && Base::time() <= $timeStart) {
// 签到打卡提醒
$this->remind('in');
}
}
if ($remindexceed > 0) {
$timeRemindexceed = $timeStart + $remindexceed;
if ($timeRemindexceed <= Base::time() && Base::time() <= $timeRemindexceed + 300) {
// 签到缺卡提醒
$this->remind('exceed');
}
}
}
public function end()
{
}
private function remind($type)
{
if (Cache::get("CheckinRemindTask:remind-" . $type) == date("Ymd")) {
return;
}
Cache::put("CheckinRemindTask:remind-" . $type, date("Ymd"), Carbon::now()->addDay());
//
$botUser = User::botGetOrCreate('check-in');
if (!$botUser) {
return;
}
// 提醒对象3天内有签到的成员在职
User::whereBot(0)->whereNull('disable_at')->chunk(100, function ($users) use ($type, $botUser) {
/** @var User $user */
foreach ($users as $user) {
if (UserCheckinRecord::whereUserid($user->userid)->where('date', date("Y-m-d"))->exists()) {
continue; // 已打卡
}
if (!UserCheckinRecord::whereUserid($user->userid)->where('created_at', '>', Carbon::now()->subDays(3))->exists()) {
continue; // 3天内没有打卡
}
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
if ($dialog) {
if ($type === 'exceed') {
$text = "<p><strong style='color:red'>缺卡提醒:</strong>上班时间到了,你还没有打卡哦~</p>";
} else {
$text = "<p><strong>打卡提醒:</strong>快到上班时间了,别忘了打卡哦~</p>";
}
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid);
}
}
});
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\UserBot;
use App\Models\WebSocketDialogMsg;
use Carbon\Carbon;
/**
* 删除机器人消息
*/
class DeleteBotMsgTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
public function start()
{
$bots = UserBot::where('clear_at', '<=', Carbon::now())->take(100)->get();
foreach ($bots as $bot) {
WebSocketDialogMsg::whereUserid($bot->bot_id)
->where('created_at', '<=', Carbon::now()->subDays($bot->clear_day))
->orderBy('id')
->chunk(1000, function ($msgs) {
$ids = $msgs->pluck('id')->toArray();
if ($ids) {
WebSocketDialogMsg::deleteMsgs($ids);
}
});
$bot->clear_at = Carbon::now()->addDays($bot->clear_day);
$bot->save();
}
}
public function end()
{
}
}

View File

@ -2,6 +2,7 @@
namespace App\Tasks;
use App\Models\TaskWorker;
use App\Models\Tmp;
use App\Models\WebSocketTmpMsg;
use Carbon\Carbon;
@ -18,6 +19,7 @@ class DeleteTmpTask extends AbstractTask
public function __construct(string $data, int $hours)
{
parent::__construct(...func_get_args());
$this->data = $data;
$this->hours = $hours;
}
@ -54,6 +56,23 @@ class DeleteTmpTask extends AbstractTask
});
}
break;
/**
* 表pre_task_worker
*/
case 'task_worker':
{
TaskWorker::onlyTrashed()
->where('deleted_at', '<', Carbon::now()->subHours($this->hours)->toDateTimeString())
->orderBy('id')
->forceDelete();
}
break;
}
}
public function end()
{
}
}

View File

@ -0,0 +1,170 @@
<?php
namespace App\Tasks;
use App\Models\Setting;
use App\Models\User;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use App\Module\Base;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class EmailNoticeTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
public function start()
{
$setting = Base::setting('emailSetting');
// 消息通知
if ($setting['notice_msg'] === 'open') {
$userMinute = intval($setting['msg_unread_user_minute']);
$groupMinute = intval($setting['msg_unread_group_minute']);
\DB::statement("SET SQL_MODE=''");
$builder = WebSocketDialogMsg::select(['web_socket_dialog_msgs.*', 'r.id as r_id', 'r.userid as r_userid'])
->join('web_socket_dialog_msg_reads as r', 'web_socket_dialog_msgs.id', '=', 'r.msg_id')
->whereNull("r.read_at")
->where("r.silence", 0)
->where("r.email", 0);
if ($userMinute > -1) {
$builder->clone()
->where("web_socket_dialog_msgs.dialog_type", "user")
->whereIn("web_socket_dialog_msgs.type", ["text", "file", "record", "meeting"])
->whereBetween("web_socket_dialog_msgs.created_at", [
Carbon::now()->subMinutes($userMinute + 10),
Carbon::now()->subMinutes($userMinute)
])
->groupBy('r_userid')
->chunkById(100, function ($rows) {
$this->unreadMsgEmail($rows, "user");
});
}
if ($groupMinute > -1) {
$builder->clone()
->where("web_socket_dialog_msgs.dialog_type", "group")
->whereIn("web_socket_dialog_msgs.type", ["text", "file", "record", "meeting"])
->whereBetween("web_socket_dialog_msgs.created_at", [
Carbon::now()->subMinutes($groupMinute + 10),
Carbon::now()->subMinutes($groupMinute)
])
->groupBy('r_userid')
->chunkById(100, function ($rows) {
$this->unreadMsgEmail($rows, "group");
});
}
}
}
public function end()
{
}
/**
* 未读消息通知
* @param $rows
* @param $dialogType
* @return void
*/
private function unreadMsgEmail($rows, $dialogType)
{
$array = $rows->groupBy('r_userid');
foreach ($array as $userid => $data) {
$data = WebSocketDialogMsg::select(['web_socket_dialog_msgs.*', 'r.id as r_id', 'r.userid as r_userid'])
->join('web_socket_dialog_msg_reads as r', 'web_socket_dialog_msgs.id', '=', 'r.msg_id')
->whereNull("r.read_at")
->where("r.silence", 0)
->where("r.email", 0)
->where("r.userid", $userid)
->where("web_socket_dialog_msgs.dialog_type", $dialogType)
->whereIn("web_socket_dialog_msgs.type", ["text", "file", "record", "meeting"])
->take(100)
->get();
if (empty($data)) {
continue;
}
$user = User::whereBot(0)->whereNull('disable_at')->find($userid);
if (empty($user)) {
continue;
}
if (!Base::isEmail($user->email)) {
continue;
}
$setting = Base::setting('emailSetting');
$msgType = $dialogType === "group" ? "群聊" : "成员";
$subject = null;
$content = view('email.unread', [
'type' => 'head',
'nickname' => $user->nickname,
'msgType' => $msgType,
'count' => count($data),
])->render();
$lists = $data->groupBy('dialog_id');
/** @var WebSocketDialogMsg[] $items */
foreach ($lists as $items) {
$dialogId = 0;
$dialogName = null;
foreach ($items as $item) {
$item->cancelAppend();
$item->userInfo = User::userid2basic($item->userid);
$item->preview = $item->previewMsg(true);
$item->preview = str_replace('<p>', '<p style="margin:0;padding:0">', $item->preview);
if (empty($dialogId)) {
$dialogId = $item->dialog_id;
}
if ($dialogName === null) {
if ($dialogType === "user" && $item->userInfo) {
if ($item->userInfo->profession) {
$dialogName = $item->userInfo->nickname . " ({$item->userInfo->profession})";
} else {
$dialogName = $item->userInfo->nickname;
}
} else {
$dialogName = $item->webSocketDialog?->getGroupName();
}
}
}
if ($subject === null) {
$count = count($lists);
if ($count > 1) {
$subject = "来自{$count}{$msgType}未读消息提醒";
} else {
$subject = "来自{$dialogName}未读消息提醒";
}
}
$content .= view('email.unread', [
'type' => 'content',
'dialogUrl' => config("app.url") . "/manage/messenger?dialog_id={$dialogId}",
'dialogName' => $dialogName,
'unread' => count($items),
'items' => $items,
])->render();
$content = str_replace("{{RemoteURL}}", config("app.url") . "/", $content);
}
try {
Setting::validateAddr($user->email, function($to) use ($content, $subject, $setting) {
Factory::mailer()
->setDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0")
->setMessage(EmailMessage::create()
->from(env('APP_NAME', 'Task') . " <{$setting['account']}>")
->to($to)
->subject($subject)
->html($content))
->send();
});
} catch (\Throwable $e) {
info("unreadMsgEmail: " . $e->getMessage());
}
WebSocketDialogMsgRead::whereIn('id', $data->pluck('r_id'))->update([
'email' => 1
]);
}
}
}

View File

@ -18,6 +18,7 @@ class IhttpTask extends AbstractTask
protected $extra;
protected $apiWebsocket;
protected $apiUserid;
protected $endPush = [];
/**
* IhttpTask constructor.
@ -27,6 +28,7 @@ class IhttpTask extends AbstractTask
*/
public function __construct($url, $post = [], $extra = [])
{
parent::__construct(...func_get_args());
$this->url = $url;
$this->post = $post;
$this->extra = $extra;
@ -53,7 +55,7 @@ class IhttpTask extends AbstractTask
$res = Ihttp::ihttp_request($this->url, $this->post, $this->extra);
if ($this->apiWebsocket && $this->apiUserid) {
$data = Base::isSuccess($res) ? Base::json2array($res['data']) : $res;
PushTask::push([
$this->endPush[] = [
'userid' => $this->apiUserid,
'msg' => [
'type' => 'apiWebsocket',
@ -61,7 +63,13 @@ class IhttpTask extends AbstractTask
'apiSuccess' => Base::isSuccess($res),
'data' => $data,
]
]);
];
}
}
public function end()
{
PushTask::push($this->endPush);
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Tasks;
use App\Module\Base;
use App\Module\Extranet;
use Cache;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 获取笑话、心灵鸡汤
*
* .env添加笑话 JUKE_KEY_JOKE
* .env添加鸡汤 JUKE_KEY_SOUP
*
* 每日小时采集1次
*/
class JokeSoupTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
public function start()
{
// 判断每小时执行一次
if (Cache::get("JokeSoupTask:YmdH") == date("YmdH")) {
return;
}
Cache::put("JokeSoupTask:YmdH", date("YmdH"), Carbon::now()->addDay());
//
$array = Base::json2array(Cache::get("JokeSoupTask:jokes"));
$data = Extranet::randJoke();
foreach ($data as $item) {
if ($text = trim($item['content'])) {
$array[] = $text;
}
}
Cache::forever("JokeSoupTask:jokes", Base::array2json(array_slice($array, -100)));
//
$array = Base::json2array(Cache::get("JokeSoupTask:soups"));
$data = Extranet::soups();
if ($data) {
$array[] = $data;
}
Cache::forever("JokeSoupTask:soups", Base::array2json(array_slice($array, -24)));
}
public function end()
{
}
}

View File

@ -16,6 +16,7 @@ class LineTask extends AbstractTask
{
protected $userid;
protected $online;
protected $endPush = [];
/**
* LineTask constructor.
@ -24,6 +25,7 @@ class LineTask extends AbstractTask
*/
public function __construct($userid, bool $online)
{
parent::__construct(...func_get_args());
$this->userid = $userid;
$this->online = $online;
}
@ -36,7 +38,7 @@ class LineTask extends AbstractTask
$fd[] = $ws->fd;
}
if ($fd) {
PushTask::push([
$this->endPush[] = [
'fd' => $fd,
'msg' => [
'type' => 'line',
@ -45,8 +47,13 @@ class LineTask extends AbstractTask
'online' => $this->online,
],
]
]);
];
}
});
}
public function end()
{
PushTask::push($this->endPush);
}
}

84
app/Tasks/LoopTask.php Normal file
View File

@ -0,0 +1,84 @@
<?php
namespace App\Tasks;
use App\Models\ProjectFlow;
use App\Models\ProjectFlowItem;
use App\Models\ProjectTask;
use App\Models\ProjectTaskUser;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 任务重复周期
*/
class LoopTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
public function start()
{
ProjectTask::whereBetween('loop_at', [
Carbon::now()->subMinutes(10),
Carbon::now()
])->chunkById(100, function ($list) {
/** @var ProjectTask $item */
foreach ($list as $item) {
try {
$task = $item->copyTask();
// 工作流
$projectFlow = ProjectFlow::whereProjectId($task->project_id)->orderByDesc('id')->first();
if ($projectFlow) {
$projectFlowItem = ProjectFlowItem::whereFlowId($projectFlow->id)->orderBy('sort')->get();
// 赋一个开始状态
foreach ($projectFlowItem as $flowItem) {
if ($flowItem->status == 'start') {
$task->flow_item_id = $flowItem->id;
$task->flow_item_name = $flowItem->status . "|" . $flowItem->name;
if ($flowItem->userids) {
$userids = array_values(array_unique($flowItem->userids));
foreach ($userids as $uid) {
ProjectTaskUser::updateInsert([
'task_id' => $task->id,
'userid' => $uid,
], [
'project_id' => $task->project_id,
'task_pid' => $task->id,
'owner' => 1,
]);
}
}
break;
}
}
}
// 新任务时间、周期
if ($task->start_at) {
$diffSecond = Carbon::parse($task->start_at)->diffInSeconds(Carbon::parse($task->end_at), true);
$task->start_at = Carbon::parse($task->loop_at);
$task->end_at = $task->start_at->clone()->addSeconds($diffSecond);
}
$task->refreshLoop(true);
$task->addLog("创建任务来自周期任务ID{$item->id}", [], $task->userid);
// 清空旧周期
$item->loop = '';
$item->loop_at = null;
$item->save();
$item->addLog("已创建新的周期任务ID{$task->id},此任务关闭周期", [], $task->userid);
} catch (\Throwable $e) {
$item->addLog("生成重复任务失败:" . $e->getMessage(), [], $item->userid);
}
}
});
}
public function end()
{
}
}

View File

@ -1,116 +0,0 @@
<?php
namespace App\Tasks;
use App\Models\ProjectTask;
use App\Models\ProjectTaskMailLog;
use App\Models\User;
use App\Module\Base;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class OverdueRemindEmailTask extends AbstractTask
{
public function __construct()
{
//
}
public function start()
{
$setting = Base::setting('emailSetting');
if ($setting['notice'] === 'open') {
$hours = floatval($setting['task_remind_hours']);
$hours2 = floatval($setting['task_remind_hours2']);
if ($hours > 0) {
ProjectTask::whereNull('complete_at')
->whereNull('archived_at')
->whereBetween("end_at", [
Carbon::now()->addMinutes($hours * 60),
Carbon::now()->addMinutes($hours * 60 + 10)
])->chunkById(100, function ($tasks) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$this->overdueBeforeAfterEmail($task, true);
}
});
}
if ($hours2 > 0) {
ProjectTask::whereNull('complete_at')
->whereNull('archived_at')
->whereBetween("end_at", [
Carbon::now()->subMinutes($hours2 * 60 + 10),
Carbon::now()->subMinutes($hours2 * 60)
])->chunkById(100, function ($tasks) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$this->overdueBeforeAfterEmail($task, false);
}
});
}
}
}
/**
* 过期前、超期后提醒
* @param ProjectTask $task
* @param $isBefore
* @return void
*/
private function overdueBeforeAfterEmail(ProjectTask $task, $isBefore)
{
$userids = $task->taskUser->where('owner', 1)->pluck('userid')->toArray();
if (empty($userids)) {
return;
}
$users = User::whereIn('userid', $userids)->get();
if (empty($users)) {
return;
}
$setting = Base::setting('emailSetting');
$hours = floatval($setting['task_remind_hours']);
$hours2 = floatval($setting['task_remind_hours2']);
/** @var User $user */
foreach ($users as $user) {
$data = [
'type' => $isBefore ? 1 : 2,
'userid' => $user->userid,
'task_id' => $task->id,
];
$emailLog = ProjectTaskMailLog::where($data)->first();
if ($emailLog) {
continue;
}
try {
if (!Base::isEmail($user->email)) {
throw new \Exception("User email '{$user->email}' address error");
}
if ($isBefore) {
$subject = env('APP_NAME') . " 任务提醒";
$content = "<p>{$user->nickname} 您好:</p><p>您有一个任务【{$task->name}】还有{$hours}小时即将超时,请及时处理。</p>";
} else {
$subject = env('APP_NAME') . " 任务过期提醒";
$content = "<p>{$user->nickname} 您好:</p><p>您的任务【{$task->name}】已经超时{$hours2}小时,请及时处理。</p>";
}
Factory::mailer()
->setDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0")
->setMessage(EmailMessage::create()
->from(env('APP_NAME', 'Task') . " <{$setting['account']}>")
->to($user->email)
->subject($subject)
->html($content))
->send();
$data['is_send'] = 1;
} catch (\Exception $e) {
$data['send_error'] = $e->getMessage();
}
$data['email'] = $user->email;
ProjectTaskMailLog::createInstance($data)->save();
}
}
}

View File

@ -19,6 +19,7 @@ class PushTask extends AbstractTask
{
protected $params;
protected $retryOffline = true;
protected $endPush = [];
/**
* PushTask constructor.
@ -26,6 +27,7 @@ class PushTask extends AbstractTask
*/
public function __construct($params = [], $retryOffline = true)
{
parent::__construct(...func_get_args());
$this->params = $params;
$this->retryOffline = $retryOffline;
}
@ -42,12 +44,41 @@ class PushTask extends AbstractTask
}
// 根据会员ID推送离线时收到的消息
elseif (Base::leftExists($this->params, "RETRY::")) {
self::sendTmpMsgForUserid(intval(Base::leftDelete($this->params, "RETRY::")));
$this->sendTmpMsgForUserid(intval(Base::leftDelete($this->params, "RETRY::")));
}
}
is_array($this->params) && self::push($this->params, $this->retryOffline);
}
public function end()
{
self::push($this->endPush);
}
/**
* 根据会员ID推送离线时收到的消息
* @param $userid
*/
private function sendTmpMsgForUserid($userid)
{
if (empty($userid)) {
return;
}
WebSocketTmpMsg::whereCreateId($userid)
->whereSend(0)
->where('created_at', '>', Carbon::now()->subMinute()) // 1分钟内添加的数据
->orderBy('id')
->chunk(100, function($list) use ($userid) {
foreach ($list as $item) {
$this->endPush[] = [
'tmpMsgId' => $item->id,
'userid' => $userid,
'msg' => Base::json2array($item->msg),
];
}
});
}
/**
* 记录离线消息,等上线后重新发送
* @param array $userFail
@ -71,30 +102,6 @@ class PushTask extends AbstractTask
}
}
/**
* 根据会员ID推送离线时收到的消息
* @param $userid
*/
private static function sendTmpMsgForUserid($userid)
{
if (empty($userid)) {
return;
}
WebSocketTmpMsg::whereCreateId($userid)
->whereSend(0)
->where('created_at', '>', Carbon::now()->subMinute()) // 1分钟内添加的数据
->orderBy('id')
->chunk(100, function($list) use ($userid) {
foreach ($list as $item) {
self::push([
'tmpMsgId' => $item->id,
'userid' => $userid,
'msg' => Base::json2array($item->msg),
]);
}
});
}
/**
* 推送消息
* @param array $lists 消息列表
@ -172,7 +179,7 @@ class PushTask extends AbstractTask
try {
$swoole->push($fid, Base::array2json($msg));
$tmpMsgId > 0 && WebSocketTmpMsg::whereId($tmpMsgId)->update(['send' => 1]);
} catch (\Exception $e) {
} catch (\Throwable) {
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\UmengAlias;
use App\Module\Base;
/**
* 推送友盟消息
*/
class PushUmengMsg extends AbstractTask
{
protected $userid = 0;
protected $array = [];
/**
* @param array|int $userid
* @param array $array
*/
public function __construct($userid, $array = [])
{
parent::__construct(...func_get_args());
$this->userid = $userid;
$this->array = is_array($array) ? $array : [];
}
public function start()
{
if (empty($this->userid) || empty($this->array)) {
return;
}
$setting = Base::setting('appPushSetting');
if ($setting['push'] !== 'open') {
return;
}
UmengAlias::pushMsgToUserid($this->userid, $this->array);
}
public function end()
{
}
}

View File

@ -8,11 +8,14 @@ use App\Models\User;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use App\Module\Base;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Request;
/**
* 推送话消息
* 推送话消息
* Class WebSocketDialogMsgTask
* @package App\Tasks
*/
@ -20,15 +23,45 @@ class WebSocketDialogMsgTask extends AbstractTask
{
protected $id;
protected $ignoreFd;
protected $msgNotExistRetry = false; // 推送失败后重试
protected $silence = false; // 静默推送前端不通知、App不推送如果会话设置了免打扰则强制静默
protected $endPush = [];
protected $endArray = [];
/**
* WebSocketDialogMsgTask constructor.
* @param int $id 消息ID
* @param mixed $ignoreFd
*/
public function __construct($id)
public function __construct($id, $ignoreFd = null)
{
parent::__construct(...func_get_args());
$this->id = $id;
$this->ignoreFd = Request::header('fd');
$this->ignoreFd = $ignoreFd === null ? Request::header('fd') : $ignoreFd;
}
/**
* @param $ignoreFd
*/
public function setIgnoreFd($ignoreFd)
{
$this->ignoreFd = $ignoreFd;
}
/**
* @param bool $msgNotExistRetry
*/
public function setMsgNotExistRetry(bool $msgNotExistRetry): void
{
$this->msgNotExistRetry = $msgNotExistRetry;
}
/**
* @param bool $silence
*/
public function setSilence(bool $silence): void
{
$this->silence = $silence;
}
public function start()
@ -37,64 +70,160 @@ class WebSocketDialogMsgTask extends AbstractTask
$_A = [
'__fill_url_remote_url' => true,
];
//
$msg = WebSocketDialogMsg::find($this->id);
if (empty($msg)) {
if ($this->msgNotExistRetry) {
$task = new WebSocketDialogMsgTask($this->id, $this->ignoreFd || '');
$task->delay(1);
$this->endArray[] = $task;
}
return;
}
$dialog = WebSocketDialog::find($msg->dialog_id);
if (empty($dialog)) {
return;
}
$updateds = [];
$silences = [];
foreach ($dialog->dialogUser as $dialogUser) {
$updateds[$dialogUser->userid] = $dialogUser->updated_at;
$silences[$dialogUser->userid] = $dialogUser->silence;
}
$userids = array_keys($silences);
// 推送目标①:群成员
$userids = $dialog->dialogUser->pluck('userid')->toArray();
foreach ($userids AS $userid) {
if ($userid == $msg->userid) {
continue;
// 提及会员
$mentions = [];
if ($msg->type === 'text') {
preg_match_all("/<span class=\"mention user\" data-id=\"(\d+)\">/", $msg->msg['text'], $matchs);
if ($matchs) {
$mentions = array_values(array_filter(array_unique($matchs[1])));
}
}
// 将会话以外的成员加入会话内
$diffids = array_values(array_diff($mentions, $userids));
if ($diffids) {
// 仅(群聊)且(是群主或没有群主)才可以@成员以外的人
if ($dialog->type === 'group' && in_array($dialog->owner_id, [0, $msg->userid])) {
$dialog->joinGroup($diffids, $msg->userid);
$dialog->pushMsg("groupJoin", null, $diffids);
$userids = array_values(array_unique(array_merge($mentions, $userids)));
}
}
// 推送目标①:会话成员/群成员
$array = [];
foreach ($userids AS $userid) {
$silence = $this->silence || $silences[$userid];
$updated = $updateds[$userid] ?? $msg->created_at;
if ($userid == $msg->userid) {
$array[$userid] = [
'userid' => $userid,
'mention' => false,
'silence' => $silence,
'updated' => $updated,
];
} else {
$mention = array_intersect([0, $userid], $mentions) ? 1 : 0;
$silence = $mention ? false : $silence;
WebSocketDialogMsgRead::createInstance([
'dialog_id' => $msg->dialog_id,
'msg_id' => $msg->id,
'userid' => $userid,
'mention' => $mention,
'silence' => $silence,
])->saveOrIgnore();
$array[$userid] = [
'userid' => $userid,
'mention' => $mention,
'silence' => $silence,
'updated' => $updated,
];
// 机器人收到消处理
$botUser = User::whereUserid($userid)->whereBot(1)->first();
if ($botUser) {
$this->endArray[] = new BotReceiveMsgTask($botUser->userid, $msg->id);
}
}
WebSocketDialogMsgRead::createInstance([
'dialog_id' => $msg->dialog_id,
'msg_id' => $msg->id,
'userid' => $userid,
])->saveOrIgnore();
}
// 更新已发送数量
$msg->send = WebSocketDialogMsgRead::whereMsgId($msg->id)->count();
$msg->save();
// 开始推送消息
PushTask::push([
'userid' => $userids,
'ignoreFd' => $this->ignoreFd,
'msg' => [
'type' => 'dialog',
'mode' => 'add',
'data' => $msg->toArray(),
]
]);
$umengUserid = [];
foreach ($array as $item) {
$this->endPush[] = [
'userid' => $item['userid'],
'ignoreFd' => $this->ignoreFd,
'msg' => [
'type' => 'dialog',
'mode' => 'add',
'silence' => $item['silence'] ? 1 : 0,
'data' => array_merge($msg->toArray(), [
'mention' => $item['mention'],
'user_at' => Carbon::parse($item['updated'])->toDateTimeString('millisecond'),
'user_ms' => Carbon::parse($item['updated'])->valueOf(),
]),
]
];
if ($item['userid'] != $msg->userid && !$item['silence'] && !$this->silence) {
$umengUserid[] = $item['userid'];
}
}
// umeng推送app
if ($umengUserid) {
$setting = Base::setting('appPushSetting');
if ($setting['push'] === 'open') {
$umengTitle = User::userid2nickname($msg->userid);
if ($dialog->type == 'group') {
$umengTitle = "{$dialog->getGroupName()} ($umengTitle)";
}
$this->endArray[] = new PushUmengMsg($umengUserid, [
'title' => $umengTitle,
'body' => $msg->previewMsg(),
'description' => "MID:{$msg->id}",
'seconds' => 3600,
'badge' => 1,
]);
}
}
// 推送目标②:正在打开这个任务会话的会员
if ($dialog->type == 'group' && $dialog->group_type == 'task') {
$list = User::whereTaskDialogId($dialog->id)->pluck('userid')->toArray();
if ($list) {
$array = [];
foreach ($list as $uid) {
if (!in_array($uid, $userids)) {
$array[] = $uid;
foreach ($list as $item) {
if (!in_array($item, $userids)) {
$array[] = $item;
}
}
if ($array) {
PushTask::push([
$this->endPush[] = [
'userid' => $array,
'ignoreFd' => $this->ignoreFd,
'msg' => [
'type' => 'dialog',
'mode' => 'chat',
'data' => $msg->toArray(),
'silence' => $this->silence ? 1 : 0,
'data' => array_merge($msg->toArray(), [
'user_at' => Carbon::parse($msg->created_at)->toDateTimeString('millisecond'),
'user_ms' => Carbon::parse($msg->created_at)->valueOf(),
]),
]
]);
];
}
}
}
}
public function end()
{
foreach ($this->endArray as $task) {
Task::deliver($task);
}
PushTask::push($this->endPush);
}
}

15
bin/auto Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
if [ ! -f ".env" ]; then
echo -e "配置文件不存在!"
exit 1
fi
debug=`cat .env | grep "^APP_DEBUG=" | awk -F '=' '{print $2}'`
if [ "$debug" = "true" ]; then
echo "[MODE] development"
./bin/inotify ./app
else
echo "[MODE] production"
php bin/laravels start -i
fi

89
bin/run
View File

@ -1,89 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Class runLoader
*/
class runLoader
{
public function randString($length = 16)
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$passwordstr = '';
$max = strlen($chars) - 1;
for ($i = 0; $i < $length; $i++) {
$passwordstr .= $chars[mt_rand(0, $max)];
}
return $passwordstr;
}
function getEnv(string $key)
{
if (empty($key) || !is_string($key)) {
return '';
}
$envPath = realpath(__DIR__ . '/../') . DIRECTORY_SEPARATOR . '.env';
if (!file_exists($envPath)) {
return false;
}
$envContent = file_get_contents($envPath);
preg_match_all("/^" . $key . "\s*=\s*(.*?)$/m", $envContent, $matchs);
return $matchs[1] ?: '';
}
function modifyEnv(array $data)
{
if (empty($data) || !is_array($data)) {
return false;
}
$envPath = realpath(__DIR__ . '/../') . DIRECTORY_SEPARATOR . '.env';
if (!file_exists($envPath)) {
return false;
}
$envContent = file_get_contents($envPath);
foreach ($data as $key => $val) {
$envContent = preg_replace("/^" . $key . "\s*=\s*(.*?)$/m", $key . "=" . $val, $envContent);
}
file_put_contents($envPath, $envContent);
return true;
}
function modifyMode($type)
{
$filePath = realpath(__DIR__ . '/../') . DIRECTORY_SEPARATOR . '/docker/php/php.conf';
if (!file_exists($filePath)) {
return false;
}
$envContent = file_get_contents($filePath);
$envContent = str_replace("#command=php bin/laravels start -i", "command=php bin/laravels start -i", $envContent);
$envContent = str_replace("#command=./bin/inotify ./app", "command=./bin/inotify ./app", $envContent);
if ($type == "dev") {
$envContent = str_replace("command=php bin/laravels start -i", "#command=php bin/laravels start -i", $envContent);
$this->modifyEnv([
'APP_DEBUG' => 'true'
]);
} else {
$envContent = str_replace("command=./bin/inotify ./app", "#command=./bin/inotify ./app", $envContent);
$this->modifyEnv([
'APP_DEBUG' => 'false'
]);
}
file_put_contents($filePath, $envContent);
return true;
}
}
$array = getopt('', ['port:', 'mode:']);
$loader = new runLoader();
if (isset($array['mode'])) {
$loader->modifyMode($array['mode']);
}
$data = [];
if (isset($array['port'])) {
$data['APP_PORT'] = $array['port'];
}
if ($data) {
$loader->modifyEnv($data);
}

60
cliff.toml Normal file
View File

@ -0,0 +1,60 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}]
{% else %}\
## [Unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^pref", group = "Performance"},
{ message = "^refactor", group = "Refactor"},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = true
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

116
cmd
View File

@ -41,15 +41,31 @@ rand_string() {
fi
}
supervisorctl_restart() {
local RES=`run_exec php "supervisorctl update $1"`
restart_php() {
local RES=`run_exec php "supervisorctl update php"`
if [ -z "$RES" ]; then
run_exec php "supervisorctl restart $1"
RES=`run_exec php "supervisorctl restart php"`
fi
local IN=`echo $RES | grep "ERROR"`
if [[ "$IN" != "" ]]; then
$COMPOSE stop php
$COMPOSE start php
else
echo -e "$RES"
fi
}
switch_debug() {
local debug="false"
if [[ "$1" == "true" ]] || [[ "$1" == "dev" ]] || [[ "$1" == "open" ]]; then
debug="true"
fi
if [[ "$(env_get APP_DEBUG)" != "$debug" ]]; then
env_set APP_DEBUG "$debug"
restart_php
fi
}
check_docker() {
docker --version &> /dev/null
if [ $? -ne 0 ]; then
@ -90,15 +106,17 @@ run_compile() {
if [ ! -d "./node_modules" ]; then
npm install
fi
run_exec php "php bin/run --mode=$type"
supervisorctl_restart php
if [ "$type" = "dev" ]; then
echo "<script>window.location.href=window.location.href.replace(/:\d+/, ':' + $(env_get APP_PORT))</script>" > ./index.html
env_set APP_DEV_PORT $(rand 20001 30000)
fi
switch_debug "$type"
#
if [ "$type" = "prod" ]; then
rm -rf "./public/js/build"
npx mix --production
echo "$(rand_string 16)" > ./public/js/hash
npx vite build -- fromcmd
else
npx mix watch --hot
npx vite -- fromcmd
fi
}
@ -117,14 +135,16 @@ run_electron() {
if [ -d "./electron/dist" ]; then
rm -rf "./electron/dist"
fi
if [ -d "./electron/public" ] && [ "$argv" != "--nobuild" ]; then
if [ -d "./electron/public" ]; then
rm -rf "./electron/public"
fi
mkdir -p ./electron/public
cp ./electron/index.html ./electron/public/index.html
#
if [ "$argv" != "dev" ] && [ "$argv" != "--nobuild" ]; then
npx mix --production -- --env --electron
if [ "$argv" == "dev" ]; then
switch_debug "$argv"
else
mkdir -p ./electron/public
cp ./electron/index.html ./electron/public/index.html
npx vite build -- fromcmd electronBuild
fi
node ./electron/build.js $argv
}
@ -239,15 +259,6 @@ arg_get() {
echo $value
}
is_arm() {
local get_arch=`arch`
if [[ $get_arch =~ "aarch" ]] || [[ $get_arch =~ "arm" ]]; then
echo "yes"
else
echo "no"
fi
}
####################################################################################
####################################################################################
####################################################################################
@ -260,11 +271,6 @@ fi
if [ $# -gt 0 ]; then
if [[ "$1" == "init" ]] || [[ "$1" == "install" ]]; then
shift 1
# 判断架构
if [[ "$(is_arm)" == "yes" ]] && [[ -z "$(arg_get force)" ]]; then
echo -e "${Error} ${RedBG}暂不支持arm架构强制安装请使用./cmd install --force${Font}"
exit 1
fi
# 初始化文件
if [[ -n "$(arg_get relock)" ]]; then
rm -rf node_modules
@ -291,9 +297,9 @@ if [ $# -gt 0 ]; then
exit 1
fi
[[ -z "$(env_get APP_KEY)" ]] && run_exec php "php artisan key:generate"
run_exec php "php bin/run --mode=prod"
switch_debug "false"
# 检查数据库
remaining=10
remaining=20
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/db.opt" ]; do
((remaining=$remaining-1))
if [ $remaining -lt 0 ]; then
@ -311,24 +317,26 @@ if [ $# -gt 0 ]; then
# 设置初始化密码
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
$COMPOSE up -d
supervisorctl_restart php
restart_php
echo -e "${OK} ${GreenBG} 安装完成 ${Font}"
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
echo -e "$res"
elif [[ "$1" == "update" ]]; then
shift 1
run_mysql backup
git fetch --all
git reset --hard origin/$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
git pull
run_exec php "composer update"
if [[ -z "$(arg_get local)" ]]; then
git fetch --all
git reset --hard origin/$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
git pull
run_exec php "composer update"
fi
run_exec php "php artisan migrate"
supervisorctl_restart php
restart_php
$COMPOSE up -d
elif [[ "$1" == "uninstall" ]]; then
shift 1
read -rp "确定要卸载(含:删除容器、数据库、日志)吗?(y/n): " uninstall
[[ -z ${uninstall} ]] && uninstall="N"
read -rp "确定要卸载(含:删除容器、数据库、日志)吗?(Y/n): " uninstall
[[ -z ${uninstall} ]] && uninstall="Y"
case $uninstall in
[yY][eE][sS] | [yY])
echo -e "${RedBG} 开始卸载... ${Font}"
@ -339,6 +347,7 @@ if [ $# -gt 0 ]; then
;;
esac
$COMPOSE down
env_set APP_DEBUG "false"
rm -rf "./docker/mysql/data"
rm -rf "./docker/log/supervisor"
find "./storage/logs" -name "*.log" | xargs rm -rf
@ -354,15 +363,30 @@ if [ $# -gt 0 ]; then
$COMPOSE up -d
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
elif [[ "$1" == "url" ]]; then
shift 1
env_set APP_URL "$1"
restart_php
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
elif [[ "$1" == "env" ]]; then
shift 1
if [ -n "$1" ]; then
env_set $1 "$2"
fi
restart_php
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
elif [[ "$1" == "repassword" ]]; then
shift 1
run_exec mariadb "sh /etc/mysql/repassword.sh \"$@\""
elif [[ "$1" == "dev" ]] || [[ "$1" == "development" ]]; then
elif [[ "$1" == "serve" ]] || [[ "$1" == "dev" ]] || [[ "$1" == "development" ]]; then
shift 1
run_compile dev
elif [[ "$1" == "prod" ]] || [[ "$1" == "production" ]]; then
elif [[ "$1" == "build" ]] || [[ "$1" == "prod" ]] || [[ "$1" == "production" ]]; then
shift 1
run_compile prod
elif [[ "$1" == "appbuild" ]] || [[ "$1" == "buildapp" ]]; then
shift 1
run_electron app $@
elif [[ "$1" == "electron" ]]; then
shift 1
run_electron $@
@ -372,12 +396,8 @@ if [ $# -gt 0 ]; then
docker run -it --rm -v ${cur_path}:/home/node/apidoc kuaifan/apidoc -i app/Http/Controllers/Api -o public/docs
elif [[ "$1" == "debug" ]]; then
shift 1
if [[ "$@" == "close" ]]; then
env_set APP_DEBUG "false"
else
env_set APP_DEBUG "true"
fi
supervisorctl_restart php
switch_debug "$@"
echo "success"
elif [[ "$1" == "https" ]]; then
shift 1
if [[ "$@" == "auto" ]]; then
@ -385,7 +405,7 @@ if [ $# -gt 0 ]; then
else
env_set APP_SCHEME "true"
fi
supervisorctl_restart php
restart_php
elif [[ "$1" == "artisan" ]]; then
shift 1
e="php artisan $@" && run_exec php "$e"
@ -418,7 +438,11 @@ if [ $# -gt 0 ]; then
e="supervisorctl $@" && run_exec php "$e"
elif [[ "$1" == "models" ]]; then
shift 1
run_exec php "php app/Models/clearHelper.php"
run_exec php "php artisan ide-helper:models -W"
elif [[ "$1" == "translate" ]]; then
shift 1
run_exec php "cd /var/www/language && php translate.php"
elif [[ "$1" == "test" ]]; then
shift 1
e="./vendor/bin/phpunit $@" && run_exec php "$e"

View File

@ -14,17 +14,22 @@
"ext-json": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"directorytree/ldaprecord-laravel": "^2.7",
"fideloper/proxy": "^4.4.1",
"fruitcake/laravel-cors": "^2.0.4",
"guanguans/notify": "^1.20",
"guanguans/notify": "^1.21.1",
"guzzlehttp/guzzle": "^7.3.0",
"laravel/framework": "^v8.48.1",
"hedeqiang/umeng": "^2.1",
"laravel/framework": "^v8.83.27",
"laravel/tinker": "^v2.6.1",
"lasserafn/php-initial-avatar-generator": "^4.2",
"maatwebsite/excel": "^3.1.31",
"madnest/madzipper": "^v1.1.0",
"mews/captcha": "^3.2.6",
"orangehill/iseed": "^3.0.1",
"predis/predis": "^1.1.7"
"overtrue/pinyin": "^4.0",
"predis/predis": "^1.1.7",
"symfony/mailer": "^6.0"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^v2.10.0",

2844
composer.lock generated

File diff suppressed because it is too large Load Diff

73
config/ldap.php Normal file
View File

@ -0,0 +1,73 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default LDAP Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the LDAP connections below you wish
| to use as your default connection for all LDAP operations. Of
| course you may add as many connections you'd like below.
|
*/
'default' => env('LDAP_CONNECTION', 'default'),
/*
|--------------------------------------------------------------------------
| LDAP Connections
|--------------------------------------------------------------------------
|
| Below you may configure each LDAP connection your application requires
| access to. Be sure to include a valid base DN - otherwise you may
| not receive any results when performing LDAP search operations.
|
*/
'connections' => [
'default' => [
'hosts' => [env('LDAP_HOST', '127.0.0.1')],
'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
'password' => env('LDAP_PASSWORD', 'secret'),
'port' => env('LDAP_PORT', 389),
'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', false),
'use_tls' => env('LDAP_TLS', false),
],
],
/*
|--------------------------------------------------------------------------
| LDAP Logging
|--------------------------------------------------------------------------
|
| When LDAP logging is enabled, all LDAP search and authentication
| operations are logged using the default application logging
| driver. This can assist in debugging issues and more.
|
*/
'logging' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| LDAP Cache
|--------------------------------------------------------------------------
|
| LDAP caching enables the ability of caching search results using the
| query builder. This is great for running expensive operations that
| may take many seconds to complete, such as a pagination request.
|
*/
'cache' => [
'enabled' => env('LDAP_CACHE', false),
'driver' => env('CACHE_DRIVER', 'file'),
],
];

View File

@ -20,7 +20,7 @@ class CreateWebSocketDialogMsgReadsTable extends Migration
$table->bigInteger('userid')->nullable()->default(0)->comment('发送会员ID');
$table->tinyInteger('after')->nullable()->default(0)->comment('在阅读之后才添加的记录');
$table->timestamp('read_at')->nullable()->comment('阅读时间');
$table->unique(['msg_id', 'userid'], 'IDEX_msg_id_userid');
$table->unique(['msg_id', 'userid']);
});
}

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