mirror of
https://github.com/kuaifan/dootask.git
synced 2026-06-26 09:12:14 +00:00
chore(upgrade): Laravel 8 直升 13(旧结构跑通)+ PHP 8.4 + 依赖升级与兼容修复
- composer: framework ^13.0、php ^8.3、laravel-s ~3.8.0、predis ^2.3、 phpunit ^11.5、tinker ^3、excel ^3.1.69、captcha ^3.5、avatar ^6.5、 ldaprecord-laravel ^4、pinyin ^5.3、notify 锁 ~1.28.0; 移除 fideloper/proxy、fruitcake/laravel-cors、facade/ignition、 laravel/sail、madnest/madzipper、手动钉的 symfony/mailer; symfony/console 锁 ^7.4(LaravelS Portal 与 console 8 的 configure(): void 类型断言不兼容) - $dates 移除:AbstractModel 改 getCasts() 合并默认 datetime 列, 3 个子模型改 $casts - Carbon 3:4 处 diffInSeconds 补 absolute 参数并取整 - LdapRecord v4:config use_ssl/use_tls→use_tls/use_starttls(env 变量名不变), LdapUser::$objectClasses 补类型声明 - Madzipper→原生 ZipArchive(Base::zipAddFiles,4 处调用) - pinyin v5 静态 API(Base::getFirstCharter/cn2pinyin) - laravolt/avatar 6.5:PatchedAvatar 修上游纵向对齐 bug (intervention 4.1.3 枚举无 middle),avatar 响应改 response()->file() - TrustProxies 改框架内置基类,CORS 改 Illuminate\Http\Middleware\HandleCors - Symfony Console 8 兼容:ManticoreSyncLock::handleSignal 新签名, pcntl 回调解耦 - 非 Swoole 运行时守卫:AbstractTask::task / PushTask::push / AbstractData(swoole table),artisan/测试上下文不再炸 Target class [swoole] does not exist - Laravel 11+ change() 丢修饰符:2023_12_07 与 2025_08_10 迁移重申 nullable/default/comment(修复 fresh 安装) - Setting/Ihttp 缺键访问加 ?? 守卫(PHP 8 警告在测试中转异常) - phpunit.xml 迁移 11 schema;UserImportParseTest 改为自建部门数据 验证:8.4 容器内 migrate:fresh --seed 213 全过;php artisan test 145 passed/1 skipped;LaravelS(Swoole 6.2.1) /health 200、登录、 token 认证、WebSocket 握手、Task 投递、头像、图片裁剪冒烟全过 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
bfa0920579
commit
645cb02757
1
.gitignore
vendored
1
.gitignore
vendored
@ -65,3 +65,4 @@ README_LOCAL.md
|
||||
|
||||
# playwright
|
||||
.playwright-mcp/
|
||||
/.phpunit.cache
|
||||
|
||||
@ -52,9 +52,18 @@ trait ManticoreSyncLock
|
||||
}
|
||||
|
||||
/**
|
||||
* 信号处理器(SIGINT/SIGTERM)
|
||||
* 信号处理器(SIGINT/SIGTERM),签名须兼容 Symfony Console 的 Command::handleSignal
|
||||
*/
|
||||
public function handleSignal(int $signal): void
|
||||
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
|
||||
{
|
||||
$this->markShouldStop();
|
||||
return false; // 继续执行,由批次循环优雅退出
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记优雅退出(pcntl 回调第二参是 siginfo,不能直接复用 handleSignal)
|
||||
*/
|
||||
private function markShouldStop(): void
|
||||
{
|
||||
$this->info("\n收到信号,将在当前批次完成后退出...");
|
||||
$this->shouldStop = true;
|
||||
@ -67,8 +76,8 @@ trait ManticoreSyncLock
|
||||
{
|
||||
if (extension_loaded('pcntl')) {
|
||||
pcntl_async_signals(true);
|
||||
pcntl_signal(SIGINT, [$this, 'handleSignal']);
|
||||
pcntl_signal(SIGTERM, [$this, 'handleSignal']);
|
||||
pcntl_signal(SIGINT, fn () => $this->markShouldStop());
|
||||
pcntl_signal(SIGTERM, fn () => $this->markShouldStop());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api;
|
||||
|
||||
use Request;
|
||||
use Response;
|
||||
use Madzipper;
|
||||
use Carbon\Carbon;
|
||||
use App\Module\Down;
|
||||
use App\Models\User;
|
||||
@ -951,7 +950,7 @@ class ApproveController extends AbstractController
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
Base::zipAddFiles($zipPath, $xlsPath);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
//
|
||||
|
||||
@ -5,7 +5,6 @@ namespace App\Http\Controllers\Api;
|
||||
use Request;
|
||||
use Redirect;
|
||||
use Response;
|
||||
use Madzipper;
|
||||
use Carbon\Carbon;
|
||||
use App\Module\Down;
|
||||
use App\Module\Doo;
|
||||
@ -2003,7 +2002,7 @@ class ProjectController extends AbstractController
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
Base::zipAddFiles($zipPath, $xlsPath);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
//
|
||||
@ -2171,7 +2170,7 @@ class ProjectController extends AbstractController
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
Base::zipAddFiles($zipPath, $xlsPath);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
//
|
||||
|
||||
@ -9,7 +9,6 @@ use App\Module\AI;
|
||||
use App\Module\Down;
|
||||
use Request;
|
||||
use Response;
|
||||
use Madzipper;
|
||||
use Carbon\Carbon;
|
||||
use App\Module\Doo;
|
||||
use App\Models\User;
|
||||
@ -1445,7 +1444,7 @@ class SystemController extends AbstractController
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
Base::zipAddFiles($zipPath, $xlsPath);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
//
|
||||
|
||||
@ -322,7 +322,7 @@ class UsersController extends AbstractController
|
||||
$expiredAtCarbon = $expiredAt ? Carbon::parse($expiredAt) : null;
|
||||
$data = [
|
||||
'expired_at' => $expiredAtCarbon?->toDateTimeString(),
|
||||
'remaining_seconds' => $expiredAtCarbon ? Carbon::now()->diffInSeconds($expiredAtCarbon, false) : null,
|
||||
'remaining_seconds' => $expiredAtCarbon ? (int)Carbon::now()->diffInSeconds($expiredAtCarbon, false) : null,
|
||||
'expired' => $expired,
|
||||
'server_time' => Carbon::now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
@ -26,7 +26,7 @@ use App\Tasks\UnclaimedTaskRemindTask;
|
||||
use App\Tasks\TodoRemindTask;
|
||||
use App\Tasks\AiTaskLoopTask;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Laravolt\Avatar\Avatar;
|
||||
use App\Module\PatchedAvatar as Avatar;
|
||||
|
||||
|
||||
/**
|
||||
@ -221,11 +221,13 @@ class IndexController extends InvokeController
|
||||
'radius' => 0,
|
||||
],
|
||||
]);
|
||||
return response($avatar->create($name)->save($file))
|
||||
->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));
|
||||
$avatar->create($name)->save($file);
|
||||
return response()->file($file, [
|
||||
'Pragma' => 'public',
|
||||
'Cache-Control' => 'max-age=1814400',
|
||||
'Content-type' => 'image/png',
|
||||
'Expires' => gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -16,7 +16,7 @@ class Kernel extends HttpKernel
|
||||
protected $middleware = [
|
||||
// \App\Http\Middleware\TrustHosts::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Fruitcake\Cors\HandleCors::class,
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Fideloper\Proxy\TrustProxies as Middleware;
|
||||
use Illuminate\Http\Middleware\TrustProxies as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TrustProxies extends Middleware
|
||||
|
||||
@ -15,10 +15,8 @@ class LdapUser extends Model
|
||||
{
|
||||
/**
|
||||
* The object classes of the LDAP model.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $objectClasses = [
|
||||
public static array $objectClasses = [
|
||||
'person',
|
||||
'top',
|
||||
];
|
||||
|
||||
@ -31,7 +31,10 @@ class AbstractModel extends Model
|
||||
|
||||
const ID = 'id';
|
||||
|
||||
protected $dates = [
|
||||
/**
|
||||
* 全局日期字段(Laravel 10 移除 $dates 属性后改经 getCasts 合并,子模型 $casts 同名键优先)
|
||||
*/
|
||||
protected $defaultDatetimeCasts = [
|
||||
'top_at',
|
||||
'last_at',
|
||||
|
||||
@ -59,6 +62,15 @@ class AbstractModel extends Model
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
public function getCasts(): array
|
||||
{
|
||||
$casts = parent::getCasts();
|
||||
foreach ($this->defaultDatetimeCasts as $field) {
|
||||
$casts[$field] ??= 'datetime';
|
||||
}
|
||||
return $casts;
|
||||
}
|
||||
|
||||
protected $appendattrs = [];
|
||||
|
||||
/**
|
||||
|
||||
@ -28,10 +28,8 @@ class ManticoreSyncFailure extends AbstractModel
|
||||
'last_retry_at',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'last_retry_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
protected $casts = [
|
||||
'last_retry_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -937,7 +937,7 @@ class ProjectTask extends AbstractModel
|
||||
'cache' => [
|
||||
'task_at' => $oldStringAt,
|
||||
'change_at' => $newStringAt,
|
||||
'over_sec' => $effectiveEndTime->diffInSeconds($oldAt[1]),
|
||||
'over_sec' => (int)$effectiveEndTime->diffInSeconds($oldAt[1], true),
|
||||
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
|
||||
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
|
||||
]
|
||||
@ -1633,7 +1633,7 @@ class ProjectTask extends AbstractModel
|
||||
$this->addLog("{任务}超期未完成", [
|
||||
'cache' => [
|
||||
'task_at' => $this->start_at . '~' . $this->end_at,
|
||||
'over_sec' => Carbon::now()->diffInSeconds($this->end_at),
|
||||
'over_sec' => (int)Carbon::now()->diffInSeconds($this->end_at, true),
|
||||
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
|
||||
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
|
||||
]
|
||||
|
||||
@ -156,7 +156,7 @@ class Report extends AbstractModel
|
||||
* @param User|null $user
|
||||
* @return Builder|Model|\Illuminate\Database\Query\Builder|object
|
||||
*/
|
||||
public static function getLastOne(User $user = null)
|
||||
public static function getLastOne(?User $user = null)
|
||||
{
|
||||
$user === null && $user = User::auth();
|
||||
$one = self::whereUserid($user->userid)->orderByDesc("created_at")->first();
|
||||
|
||||
@ -51,12 +51,12 @@ class Setting extends AbstractModel
|
||||
switch ($this->name) {
|
||||
// 系统设置
|
||||
case 'system':
|
||||
$value['system_alias'] = $value['system_alias'] ?: env('APP_NAME');
|
||||
$value['image_compress'] = $value['image_compress'] ?: 'open';
|
||||
$value['image_quality'] = min(100, max(0, intval($value['image_quality']) ?: 90));
|
||||
$value['image_save_local'] = $value['image_save_local'] ?: 'open';
|
||||
$value['task_user_limit'] = min(2000, max(1, intval($value['task_user_limit']) ?: 500));
|
||||
if (!is_array($value['task_default_time']) || count($value['task_default_time']) != 2 || !Timer::isTime($value['task_default_time'][0]) || !Timer::isTime($value['task_default_time'][1])) {
|
||||
$value['system_alias'] = ($value['system_alias'] ?? null) ?: env('APP_NAME');
|
||||
$value['image_compress'] = ($value['image_compress'] ?? null) ?: 'open';
|
||||
$value['image_quality'] = min(100, max(0, intval($value['image_quality'] ?? 0) ?: 90));
|
||||
$value['image_save_local'] = ($value['image_save_local'] ?? null) ?: 'open';
|
||||
$value['task_user_limit'] = min(2000, max(1, intval($value['task_user_limit'] ?? 0) ?: 500));
|
||||
if (!is_array($value['task_default_time'] ?? null) || count($value['task_default_time']) != 2 || !Timer::isTime($value['task_default_time'][0]) || !Timer::isTime($value['task_default_time'][1])) {
|
||||
$value['task_default_time'] = ['09:00', '18:00'];
|
||||
}
|
||||
// 项目创建权限:范围(all/departmentOwner/appoint,默认 all)+ 指定人员
|
||||
@ -71,8 +71,8 @@ class Setting extends AbstractModel
|
||||
|
||||
// 文件设置
|
||||
case 'fileSetting':
|
||||
$value['permission_pack_type'] = $value['permission_pack_type'] ?: 'all';
|
||||
$value['permission_pack_userids'] = is_array($value['permission_pack_userids']) ? $value['permission_pack_userids'] : [];
|
||||
$value['permission_pack_type'] = ($value['permission_pack_type'] ?? null) ?: 'all';
|
||||
$value['permission_pack_userids'] = is_array($value['permission_pack_userids'] ?? null) ? $value['permission_pack_userids'] : [];
|
||||
break;
|
||||
|
||||
// AI 机器人设置
|
||||
|
||||
@ -57,8 +57,8 @@ class UserRecentItem extends AbstractModel
|
||||
'browsed_at',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'browsed_at',
|
||||
protected $casts = [
|
||||
'browsed_at' => 'datetime',
|
||||
];
|
||||
|
||||
public static function record(int $userid, string $targetType, int $targetId, string $sourceType = '', int $sourceId = 0): self
|
||||
|
||||
@ -40,8 +40,8 @@ class UserTaskBrowse extends AbstractModel
|
||||
'browsed_at',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'browsed_at',
|
||||
protected $casts = [
|
||||
'browsed_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -2589,6 +2589,23 @@ class Base
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 zip 压缩包并添加文件,条目名取文件 basename(等价旧 Madzipper::make()->add()->close())
|
||||
* @param string $zipPath 压缩包路径
|
||||
* @param string|array $files 要添加的文件路径
|
||||
*/
|
||||
public static function zipAddFiles($zipPath, $files)
|
||||
{
|
||||
$zip = new \ZipArchive();
|
||||
if ($zip->open($zipPath, \ZipArchive::CREATE) !== true) {
|
||||
throw new \RuntimeException("Unable to open zip file: " . $zipPath);
|
||||
}
|
||||
foreach ((array)$files as $file) {
|
||||
$zip->addFile($file, basename($file));
|
||||
}
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中文字符拼音首字母
|
||||
* @param $str
|
||||
@ -2604,8 +2621,7 @@ class Base
|
||||
return '#';
|
||||
}
|
||||
if (!preg_match("/^[a-zA-Z]$/", $first)) {
|
||||
$pinyin = new Pinyin();
|
||||
$first = $pinyin->abbr($first, '', PINYIN_NAME);
|
||||
$first = Pinyin::abbr($first, true)->join('');
|
||||
}
|
||||
return $first ? strtoupper($first) : '#';
|
||||
}
|
||||
@ -2623,8 +2639,7 @@ class Base
|
||||
}
|
||||
if (!preg_match("/^[a-zA-Z0-9_.]+$/", $str)) {
|
||||
$str = Cache::rememberForever("cn2pinyin:" . md5($str . '_' . $delim), function () use ($delim, $str) {
|
||||
$pinyin = new Pinyin();
|
||||
return $pinyin->permalink($str, $delim);
|
||||
return Pinyin::permalink($str, $delim);
|
||||
});
|
||||
}
|
||||
return $str;
|
||||
|
||||
@ -14,6 +14,8 @@ class Ihttp
|
||||
}
|
||||
if(!empty($urlset['query'])) {
|
||||
$urlset['query'] = "?{$urlset['query']}";
|
||||
} else {
|
||||
$urlset['query'] = '';
|
||||
}
|
||||
if(empty($urlset['port'])) {
|
||||
$urlset['port'] = $urlset['scheme'] == 'https' ? '443' : '80';
|
||||
|
||||
48
app/Module/PatchedAvatar.php
Normal file
48
app/Module/PatchedAvatar.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
|
||||
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Intervention\Image\Typography\FontFactory;
|
||||
use Laravolt\Avatar\Avatar;
|
||||
|
||||
/**
|
||||
* laravolt/avatar 6.5.0 的 buildAvatar 给纵向对齐传 'middle',
|
||||
* 而 intervention/image 4.1.3 的 Alignment 枚举仅接受 'center',会抛
|
||||
* InvalidArgumentException(Invalid value for alignment)。上游修复前以子类覆写修正。
|
||||
*/
|
||||
class PatchedAvatar extends Avatar
|
||||
{
|
||||
public function buildAvatar(): static
|
||||
{
|
||||
$this->buildInitial();
|
||||
|
||||
$x = $this->width / 2;
|
||||
$y = $this->height / 2;
|
||||
|
||||
$driver = $this->driver === 'gd' ? new GdDriver : new ImagickDriver;
|
||||
$this->image = ImageManager::usingDriver($driver)->createImage($this->width, $this->height);
|
||||
|
||||
$this->createShape();
|
||||
|
||||
if (empty($this->initials)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->image->text(
|
||||
$this->initials,
|
||||
(int) $x,
|
||||
(int) $y,
|
||||
function (FontFactory $font) {
|
||||
$font->filepath($this->font);
|
||||
$font->size($this->fontSize);
|
||||
$font->color($this->foreground);
|
||||
$font->align('center', 'center');
|
||||
}
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@ -24,7 +24,8 @@ abstract class AbstractData
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
$this->table = app('swoole')->{$this->getTableName()};
|
||||
// 非 Swoole 运行时(artisan/测试)无 swoole 绑定,table 为 null,各方法返回默认值
|
||||
$this->table = app()->bound('swoole') ? app('swoole')->{$this->getTableName()} : null;
|
||||
}
|
||||
|
||||
public function getTable()
|
||||
@ -42,22 +43,34 @@ abstract class AbstractData
|
||||
|
||||
public static function set($key, $value)
|
||||
{
|
||||
if (!self::instance()->table) {
|
||||
return false;
|
||||
}
|
||||
return self::instance()->table->set($key, ['value' => $value]);
|
||||
}
|
||||
|
||||
public static function get($key, $default = null)
|
||||
{
|
||||
if (!self::instance()->table) {
|
||||
return $default;
|
||||
}
|
||||
$data = self::instance()->table->get($key);
|
||||
return $data ? $data['value'] : $default;
|
||||
}
|
||||
|
||||
public static function del($key)
|
||||
{
|
||||
if (!self::instance()->table) {
|
||||
return false;
|
||||
}
|
||||
return self::instance()->table->del($key);
|
||||
}
|
||||
|
||||
public static function exist($key)
|
||||
{
|
||||
if (!self::instance()->table) {
|
||||
return false;
|
||||
}
|
||||
return self::instance()->table->exist($key);
|
||||
}
|
||||
|
||||
@ -70,6 +83,9 @@ abstract class AbstractData
|
||||
|
||||
public static function clear()
|
||||
{
|
||||
if (!self::instance()->table) {
|
||||
return;
|
||||
}
|
||||
foreach (self::instance()->table as $key => $row) {
|
||||
self::del($key);
|
||||
}
|
||||
@ -77,6 +93,9 @@ abstract class AbstractData
|
||||
|
||||
public static function getAll()
|
||||
{
|
||||
if (!self::instance()->table) {
|
||||
return [];
|
||||
}
|
||||
$result = [];
|
||||
foreach (self::instance()->table as $key => $row) {
|
||||
$result[$key] = $row['value'];
|
||||
|
||||
@ -17,6 +17,9 @@ class OnlineData extends AbstractData
|
||||
*/
|
||||
public static function online($userid)
|
||||
{
|
||||
if (!self::instance()->getTable()) {
|
||||
return 0;
|
||||
}
|
||||
$key = "online::" . $userid;
|
||||
$value = self::instance()->getTable()->incr($key, 'value');
|
||||
if ($value === 1) {
|
||||
@ -35,6 +38,9 @@ class OnlineData extends AbstractData
|
||||
*/
|
||||
public static function offline($userid)
|
||||
{
|
||||
if (!self::instance()->getTable()) {
|
||||
return 0;
|
||||
}
|
||||
$key = "online::" . $userid;
|
||||
$value = self::instance()->getTable()->decr($key, 'value');
|
||||
if ($value === 0) {
|
||||
@ -57,6 +63,9 @@ class OnlineData extends AbstractData
|
||||
*/
|
||||
public static function live($userid)
|
||||
{
|
||||
if (!self::instance()->getTable()) {
|
||||
return 0;
|
||||
}
|
||||
$key = "online::" . $userid;
|
||||
return intval(self::instance()->getTable()->get($key));
|
||||
}
|
||||
|
||||
@ -29,6 +29,19 @@ abstract class AbstractTask extends Task
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写投递:非 Swoole 运行时(artisan/测试)无 swoole 绑定,无法投递异步任务,跳过(与 AbstractObserver 守卫一致)
|
||||
* @param mixed $task
|
||||
* @return bool
|
||||
*/
|
||||
protected function task($task)
|
||||
{
|
||||
if (!app()->bound('swoole')) {
|
||||
return false;
|
||||
}
|
||||
return parent::task($task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始执行任务
|
||||
*/
|
||||
|
||||
@ -60,7 +60,7 @@ class LoopTask extends AbstractTask
|
||||
}
|
||||
// 新任务时间、周期
|
||||
if ($task->start_at) {
|
||||
$diffSecond = Carbon::parse($task->start_at)->diffInSeconds(Carbon::parse($task->end_at), true);
|
||||
$diffSecond = (int)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);
|
||||
}
|
||||
|
||||
@ -116,6 +116,10 @@ class PushTask extends AbstractTask
|
||||
if (!Base::isTwoArray($lists)) {
|
||||
$lists = [$lists];
|
||||
}
|
||||
// 非 Swoole 运行时(artisan/测试)无 swoole 绑定,无法推送,直接跳过(与 AbstractObserver 守卫一致)
|
||||
if (!app()->bound('swoole')) {
|
||||
return;
|
||||
}
|
||||
$swoole = app('swoole');
|
||||
foreach ($lists AS $item) {
|
||||
if (!is_array($item) || empty($item)) {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"php": "^8.3",
|
||||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-ffi": "*",
|
||||
@ -20,40 +20,35 @@
|
||||
"ext-openssl": "*",
|
||||
"ext-simplexml": "*",
|
||||
"ext-zip": "*",
|
||||
"directorytree/ldaprecord-laravel": "^2.7",
|
||||
"fideloper/proxy": "^4.4.1",
|
||||
"directorytree/ldaprecord-laravel": "^4.0",
|
||||
"firebase/php-jwt": "^6.9",
|
||||
"fruitcake/laravel-cors": "^2.0.4",
|
||||
"guanguans/notify": "^1.21.1",
|
||||
"guanguans/notify": "~1.28.0",
|
||||
"guzzlehttp/guzzle": "^7.3.0",
|
||||
"hedeqiang/umeng": "^2.1",
|
||||
"laravel/framework": "^v8.83.27",
|
||||
"laravel/tinker": "^v2.6.1",
|
||||
"laravolt/avatar": "^5.1",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/tinker": "^3.0",
|
||||
"laravolt/avatar": "^6.5",
|
||||
"league/commonmark": "^2.5",
|
||||
"league/html-to-markdown": "^5.1",
|
||||
"maatwebsite/excel": "^3.1.31",
|
||||
"madnest/madzipper": "^v1.1.0",
|
||||
"maatwebsite/excel": "^3.1.69",
|
||||
"matomo/device-detector": "^6.4",
|
||||
"mews/captcha": "^3.2.6",
|
||||
"orangehill/iseed": "^3.0.1",
|
||||
"overtrue/pinyin": "^4.0",
|
||||
"phpoffice/phppresentation": "^1.1",
|
||||
"phpoffice/phpword": "^1.3",
|
||||
"predis/predis": "^1.1.7",
|
||||
"mews/captcha": "^3.5",
|
||||
"orangehill/iseed": "^3.8",
|
||||
"overtrue/pinyin": "^5.3",
|
||||
"phpoffice/phppresentation": "^1.2",
|
||||
"phpoffice/phpword": "^1.4",
|
||||
"predis/predis": "^2.3",
|
||||
"smalot/pdfparser": "^2.11",
|
||||
"symfony/mailer": "^6.0"
|
||||
"symfony/console": "^7.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-ide-helper": "^v2.10.0",
|
||||
"facade/ignition": "^2.10.2",
|
||||
"fakerphp/faker": "^v1.14.1",
|
||||
"hhxsv5/laravel-s": "^v3.7.19",
|
||||
"kitloong/laravel-migrations-generator": "^4.4.2",
|
||||
"laravel/sail": "^v1.8.1",
|
||||
"mockery/mockery": "^1.4.3",
|
||||
"nunomaduro/collision": "^v5.5.0",
|
||||
"phpunit/phpunit": "^9.5.6"
|
||||
"barryvdh/laravel-ide-helper": "^3.7",
|
||||
"fakerphp/faker": "^1.24",
|
||||
"hhxsv5/laravel-s": "~3.8.0",
|
||||
"kitloong/laravel-migrations-generator": "^7.4",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^11.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@ -95,7 +90,7 @@
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"repositories": {
|
||||
}
|
||||
|
||||
5347
composer.lock
generated
5347
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -35,8 +35,9 @@ return [
|
||||
'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),
|
||||
// LdapRecord v4:use_tls=ldaps(沿用旧 LDAP_SSL 变量),use_starttls=StartTLS(沿用旧 LDAP_TLS 变量)
|
||||
'use_tls' => env('LDAP_SSL', false),
|
||||
'use_starttls' => env('LDAP_TLS', false),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
@ -23,18 +23,19 @@ class UpdateOwnerAddIndexSome20231217 extends Migration
|
||||
$table->index('project_id');
|
||||
$table->index(['project_id','userid']);
|
||||
$table->index('owner');
|
||||
$table->integer('owner')->change();
|
||||
// Laravel 11+ 的 change() 会丢弃未声明的修饰符,须重申 nullable/default/comment
|
||||
$table->integer('owner')->nullable()->default(0)->comment('是否负责人')->change();
|
||||
});
|
||||
Schema::table('project_tasks', function (Blueprint $table) {
|
||||
$table->index('parent_id');
|
||||
$table->index('dialog_id');
|
||||
$table->index('userid');
|
||||
$table->integer('visibility')->change();
|
||||
$table->integer('visibility')->nullable()->default(1)->comment('任务可见性:1-项目人员 2-任务人员 3-指定成员')->change();
|
||||
});
|
||||
Schema::table('project_task_users', function (Blueprint $table) {
|
||||
$table->index(['task_id','userid']);
|
||||
$table->index('owner');
|
||||
$table->integer('owner')->change();
|
||||
$table->integer('owner')->nullable()->default(0)->comment('是否任务负责人')->change();
|
||||
});
|
||||
Schema::table('project_task_files', function (Blueprint $table) {
|
||||
$table->index('project_id');
|
||||
@ -63,16 +64,16 @@ class UpdateOwnerAddIndexSome20231217 extends Migration
|
||||
$table->index('link_id');
|
||||
});
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
$table->integer('link')->change();
|
||||
$table->integer('modify')->change();
|
||||
$table->integer('forward_show')->change();
|
||||
$table->integer('link')->nullable()->default(0)->comment('是否存在链接')->change();
|
||||
$table->integer('modify')->nullable()->default(0)->comment('是否编辑')->change();
|
||||
$table->integer('forward_show')->nullable()->default(1)->comment('是否显示转发的来源')->change();
|
||||
});
|
||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
||||
$table->index('dialog_id');
|
||||
$table->index('userid');
|
||||
$table->integer('mark_unread')->change();
|
||||
$table->integer('silence')->change();
|
||||
$table->integer('important')->change();
|
||||
$table->integer('mark_unread')->nullable()->default(0)->comment('是否标记为未读:0否,1是')->change();
|
||||
$table->integer('silence')->nullable()->default(0)->comment('是否免打扰:0否,1是')->change();
|
||||
$table->integer('important')->nullable()->default(0)->comment('是否不可移出(项目、任务、部门人员)')->change();
|
||||
});
|
||||
Schema::table('web_socket_dialog_msg_todos', function (Blueprint $table) {
|
||||
$table->index('msg_id');
|
||||
@ -80,22 +81,22 @@ class UpdateOwnerAddIndexSome20231217 extends Migration
|
||||
});
|
||||
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
|
||||
$table->index('dialog_id');
|
||||
$table->integer('mention')->change();
|
||||
$table->integer('silence')->change();
|
||||
$table->integer('email')->change();
|
||||
$table->integer('after')->change();
|
||||
$table->integer('mention')->nullable()->default(0)->comment('是否提及(被@)')->change();
|
||||
$table->integer('silence')->nullable()->default(0)->comment('是否免打扰:0否,1是')->change();
|
||||
$table->integer('email')->nullable()->default(0)->comment('是否发了邮件')->change();
|
||||
$table->integer('after')->nullable()->default(0)->comment('在阅读之后才添加的记录')->change();
|
||||
});
|
||||
|
||||
// 文件相关
|
||||
Schema::table('files', function (Blueprint $table) {
|
||||
$table->index('pid');
|
||||
$table->index('cid');
|
||||
$table->integer('share')->change();
|
||||
$table->integer('share')->nullable()->default(0)->comment('是否共享')->change();
|
||||
});
|
||||
Schema::table('file_users', function (Blueprint $table) {
|
||||
$table->index('file_id');
|
||||
$table->index('userid');
|
||||
$table->integer('permission')->change();
|
||||
$table->integer('permission')->nullable()->default(0)->comment('权限:0只读,1读写')->change();
|
||||
});
|
||||
Schema::table('file_links', function (Blueprint $table) {
|
||||
$table->index('file_id');
|
||||
|
||||
@ -14,7 +14,8 @@ class UpdateFilesNameLengthTo200 extends Migration
|
||||
public function up()
|
||||
{
|
||||
Schema::table('files', function (Blueprint $table) {
|
||||
$table->string('name', 255)->change();
|
||||
// Laravel 11+ 的 change() 会丢弃未声明的修饰符,须重申 nullable/default/comment
|
||||
$table->string('name', 255)->nullable()->default('')->comment('名称')->change();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
54
phpunit.xml
54
phpunit.xml
@ -1,31 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">./tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory suffix="Test.php">./tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">./app</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
<php>
|
||||
<server name="APP_ENV" value="testing"/>
|
||||
<server name="BCRYPT_ROUNDS" value="4"/>
|
||||
<server name="CACHE_DRIVER" value="array"/>
|
||||
<!-- <server name="DB_CONNECTION" value="sqlite"/> -->
|
||||
<!-- <server name="DB_DATABASE" value=":memory:"/> -->
|
||||
<server name="MAIL_MAILER" value="array"/>
|
||||
<server name="QUEUE_CONNECTION" value="sync"/>
|
||||
<server name="SESSION_DRIVER" value="array"/>
|
||||
<server name="TELESCOPE_ENABLED" value="false"/>
|
||||
</php>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" cacheDirectory=".phpunit.cache">
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">./tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory suffix="Test.php">./tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<server name="APP_ENV" value="testing"/>
|
||||
<server name="BCRYPT_ROUNDS" value="4"/>
|
||||
<server name="CACHE_DRIVER" value="array"/>
|
||||
<!-- <server name="DB_CONNECTION" value="sqlite"/> -->
|
||||
<!-- <server name="DB_DATABASE" value=":memory:"/> -->
|
||||
<server name="MAIL_MAILER" value="array"/>
|
||||
<server name="QUEUE_CONNECTION" value="sync"/>
|
||||
<server name="SESSION_DRIVER" value="array"/>
|
||||
<server name="TELESCOPE_ENABLED" value="false"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">./app</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
|
||||
@ -3,10 +3,14 @@
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserDepartment;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserImportParseTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_parse_skips_header_and_empty_rows()
|
||||
{
|
||||
$sheet = [
|
||||
@ -98,8 +102,14 @@ class UserImportParseTest extends TestCase
|
||||
// 空/非数组 → 返回空数组
|
||||
$this->assertSame([], User::assertValidDepartments([]));
|
||||
$this->assertSame([], User::assertValidDepartments('not-array'));
|
||||
// 去重 + 转 int + 过滤非正数(这些路径不查库)
|
||||
$this->assertSame([3, 5], User::assertValidDepartments(['3', 3, 5, 0, -1]));
|
||||
// 去重 + 转 int + 过滤非正数(存在性校验会查库,需用真实部门 ID)
|
||||
$deptA = UserDepartment::createInstance(['name' => 'ImportParseDeptA_' . uniqid()]);
|
||||
$deptA->save();
|
||||
$deptB = UserDepartment::createInstance(['name' => 'ImportParseDeptB_' . uniqid()]);
|
||||
$deptB->save();
|
||||
$a = $deptA->id;
|
||||
$b = $deptB->id;
|
||||
$this->assertSame([$a, $b], User::assertValidDepartments([(string)$a, $a, $b, 0, -1]));
|
||||
}
|
||||
|
||||
public function test_assert_valid_departments_rejects_over_limit()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user