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:
kuaifan 2026-06-12 19:41:59 +00:00
parent bfa0920579
commit 645cb02757
32 changed files with 2708 additions and 3020 deletions

1
.gitignore vendored
View File

@ -65,3 +65,4 @@ README_LOCAL.md
# playwright # playwright
.playwright-mcp/ .playwright-mcp/
/.phpunit.cache

View File

@ -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->info("\n收到信号,将在当前批次完成后退出...");
$this->shouldStop = true; $this->shouldStop = true;
@ -67,8 +76,8 @@ trait ManticoreSyncLock
{ {
if (extension_loaded('pcntl')) { if (extension_loaded('pcntl')) {
pcntl_async_signals(true); pcntl_async_signals(true);
pcntl_signal(SIGINT, [$this, 'handleSignal']); pcntl_signal(SIGINT, fn () => $this->markShouldStop());
pcntl_signal(SIGTERM, [$this, 'handleSignal']); pcntl_signal(SIGTERM, fn () => $this->markShouldStop());
} }
} }

View File

@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api;
use Request; use Request;
use Response; use Response;
use Madzipper;
use Carbon\Carbon; use Carbon\Carbon;
use App\Module\Down; use App\Module\Down;
use App\Models\User; use App\Models\User;
@ -951,7 +950,7 @@ class ApproveController extends AbstractController
Base::deleteDirAndFile($zipPath, true); Base::deleteDirAndFile($zipPath, true);
} }
try { try {
Madzipper::make($zipPath)->add($xlsPath)->close(); Base::zipAddFiles($zipPath, $xlsPath);
} catch (\Throwable) { } catch (\Throwable) {
} }
// //

View File

@ -5,7 +5,6 @@ namespace App\Http\Controllers\Api;
use Request; use Request;
use Redirect; use Redirect;
use Response; use Response;
use Madzipper;
use Carbon\Carbon; use Carbon\Carbon;
use App\Module\Down; use App\Module\Down;
use App\Module\Doo; use App\Module\Doo;
@ -2003,7 +2002,7 @@ class ProjectController extends AbstractController
Base::deleteDirAndFile($zipPath, true); Base::deleteDirAndFile($zipPath, true);
} }
try { try {
Madzipper::make($zipPath)->add($xlsPath)->close(); Base::zipAddFiles($zipPath, $xlsPath);
} catch (\Throwable) { } catch (\Throwable) {
} }
// //
@ -2171,7 +2170,7 @@ class ProjectController extends AbstractController
Base::deleteDirAndFile($zipPath, true); Base::deleteDirAndFile($zipPath, true);
} }
try { try {
Madzipper::make($zipPath)->add($xlsPath)->close(); Base::zipAddFiles($zipPath, $xlsPath);
} catch (\Throwable) { } catch (\Throwable) {
} }
// //

View File

@ -9,7 +9,6 @@ use App\Module\AI;
use App\Module\Down; use App\Module\Down;
use Request; use Request;
use Response; use Response;
use Madzipper;
use Carbon\Carbon; use Carbon\Carbon;
use App\Module\Doo; use App\Module\Doo;
use App\Models\User; use App\Models\User;
@ -1445,7 +1444,7 @@ class SystemController extends AbstractController
Base::deleteDirAndFile($zipPath, true); Base::deleteDirAndFile($zipPath, true);
} }
try { try {
Madzipper::make($zipPath)->add($xlsPath)->close(); Base::zipAddFiles($zipPath, $xlsPath);
} catch (\Throwable) { } catch (\Throwable) {
} }
// //

View File

@ -322,7 +322,7 @@ class UsersController extends AbstractController
$expiredAtCarbon = $expiredAt ? Carbon::parse($expiredAt) : null; $expiredAtCarbon = $expiredAt ? Carbon::parse($expiredAt) : null;
$data = [ $data = [
'expired_at' => $expiredAtCarbon?->toDateTimeString(), '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, 'expired' => $expired,
'server_time' => Carbon::now()->toDateTimeString(), 'server_time' => Carbon::now()->toDateTimeString(),
]; ];

View File

@ -26,7 +26,7 @@ use App\Tasks\UnclaimedTaskRemindTask;
use App\Tasks\TodoRemindTask; use App\Tasks\TodoRemindTask;
use App\Tasks\AiTaskLoopTask; use App\Tasks\AiTaskLoopTask;
use Hhxsv5\LaravelS\Swoole\Task\Task; 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, 'radius' => 0,
], ],
]); ]);
return response($avatar->create($name)->save($file)) $avatar->create($name)->save($file);
->header('Pragma', 'public') return response()->file($file, [
->header('Cache-Control', 'max-age=1814400') 'Pragma' => 'public',
->header('Content-type', 'image/png') 'Cache-Control' => 'max-age=1814400',
->header('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400)); 'Content-type' => 'image/png',
'Expires' => gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400),
]);
} }
/** /**

View File

@ -16,7 +16,7 @@ class Kernel extends HttpKernel
protected $middleware = [ protected $middleware = [
// \App\Http\Middleware\TrustHosts::class, // \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class, \App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class, \Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class, \App\Http\Middleware\TrimStrings::class,

View File

@ -2,7 +2,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Fideloper\Proxy\TrustProxies as Middleware; use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class TrustProxies extends Middleware class TrustProxies extends Middleware

View File

@ -15,10 +15,8 @@ class LdapUser extends Model
{ {
/** /**
* The object classes of the LDAP model. * The object classes of the LDAP model.
*
* @var array
*/ */
public static $objectClasses = [ public static array $objectClasses = [
'person', 'person',
'top', 'top',
]; ];

View File

@ -31,7 +31,10 @@ class AbstractModel extends Model
const ID = 'id'; const ID = 'id';
protected $dates = [ /**
* 全局日期字段Laravel 10 移除 $dates 属性后改经 getCasts 合并,子模型 $casts 同名键优先)
*/
protected $defaultDatetimeCasts = [
'top_at', 'top_at',
'last_at', 'last_at',
@ -59,6 +62,15 @@ class AbstractModel extends Model
'deleted_at', 'deleted_at',
]; ];
public function getCasts(): array
{
$casts = parent::getCasts();
foreach ($this->defaultDatetimeCasts as $field) {
$casts[$field] ??= 'datetime';
}
return $casts;
}
protected $appendattrs = []; protected $appendattrs = [];
/** /**

View File

@ -28,10 +28,8 @@ class ManticoreSyncFailure extends AbstractModel
'last_retry_at', 'last_retry_at',
]; ];
protected $dates = [ protected $casts = [
'last_retry_at', 'last_retry_at' => 'datetime',
'created_at',
'updated_at',
]; ];
/** /**

View File

@ -937,7 +937,7 @@ class ProjectTask extends AbstractModel
'cache' => [ 'cache' => [
'task_at' => $oldStringAt, 'task_at' => $oldStringAt,
'change_at' => $newStringAt, '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(), 'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(), 'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
] ]
@ -1633,7 +1633,7 @@ class ProjectTask extends AbstractModel
$this->addLog("{任务}超期未完成", [ $this->addLog("{任务}超期未完成", [
'cache' => [ 'cache' => [
'task_at' => $this->start_at . '~' . $this->end_at, '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(), 'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(), 'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
] ]

View File

@ -156,7 +156,7 @@ class Report extends AbstractModel
* @param User|null $user * @param User|null $user
* @return Builder|Model|\Illuminate\Database\Query\Builder|object * @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(); $user === null && $user = User::auth();
$one = self::whereUserid($user->userid)->orderByDesc("created_at")->first(); $one = self::whereUserid($user->userid)->orderByDesc("created_at")->first();

View File

@ -51,12 +51,12 @@ class Setting extends AbstractModel
switch ($this->name) { switch ($this->name) {
// 系统设置 // 系统设置
case 'system': case 'system':
$value['system_alias'] = $value['system_alias'] ?: env('APP_NAME'); $value['system_alias'] = ($value['system_alias'] ?? null) ?: env('APP_NAME');
$value['image_compress'] = $value['image_compress'] ?: 'open'; $value['image_compress'] = ($value['image_compress'] ?? null) ?: 'open';
$value['image_quality'] = min(100, max(0, intval($value['image_quality']) ?: 90)); $value['image_quality'] = min(100, max(0, intval($value['image_quality'] ?? 0) ?: 90));
$value['image_save_local'] = $value['image_save_local'] ?: 'open'; $value['image_save_local'] = ($value['image_save_local'] ?? null) ?: 'open';
$value['task_user_limit'] = min(2000, max(1, intval($value['task_user_limit']) ?: 500)); $value['task_user_limit'] = min(2000, max(1, intval($value['task_user_limit'] ?? 0) ?: 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])) { 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']; $value['task_default_time'] = ['09:00', '18:00'];
} }
// 项目创建权限范围all/departmentOwner/appoint默认 all+ 指定人员 // 项目创建权限范围all/departmentOwner/appoint默认 all+ 指定人员
@ -71,8 +71,8 @@ class Setting extends AbstractModel
// 文件设置 // 文件设置
case 'fileSetting': case 'fileSetting':
$value['permission_pack_type'] = $value['permission_pack_type'] ?: 'all'; $value['permission_pack_type'] = ($value['permission_pack_type'] ?? null) ?: 'all';
$value['permission_pack_userids'] = is_array($value['permission_pack_userids']) ? $value['permission_pack_userids'] : []; $value['permission_pack_userids'] = is_array($value['permission_pack_userids'] ?? null) ? $value['permission_pack_userids'] : [];
break; break;
// AI 机器人设置 // AI 机器人设置

View File

@ -57,8 +57,8 @@ class UserRecentItem extends AbstractModel
'browsed_at', 'browsed_at',
]; ];
protected $dates = [ protected $casts = [
'browsed_at', 'browsed_at' => 'datetime',
]; ];
public static function record(int $userid, string $targetType, int $targetId, string $sourceType = '', int $sourceId = 0): self public static function record(int $userid, string $targetType, int $targetId, string $sourceType = '', int $sourceId = 0): self

View File

@ -40,8 +40,8 @@ class UserTaskBrowse extends AbstractModel
'browsed_at', 'browsed_at',
]; ];
protected $dates = [ protected $casts = [
'browsed_at', 'browsed_at' => 'datetime',
]; ];
/** /**

View File

@ -2589,6 +2589,23 @@ class Base
return $array; 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 * @param $str
@ -2604,8 +2621,7 @@ class Base
return '#'; return '#';
} }
if (!preg_match("/^[a-zA-Z]$/", $first)) { if (!preg_match("/^[a-zA-Z]$/", $first)) {
$pinyin = new Pinyin(); $first = Pinyin::abbr($first, true)->join('');
$first = $pinyin->abbr($first, '', PINYIN_NAME);
} }
return $first ? strtoupper($first) : '#'; return $first ? strtoupper($first) : '#';
} }
@ -2623,8 +2639,7 @@ class Base
} }
if (!preg_match("/^[a-zA-Z0-9_.]+$/", $str)) { if (!preg_match("/^[a-zA-Z0-9_.]+$/", $str)) {
$str = Cache::rememberForever("cn2pinyin:" . md5($str . '_' . $delim), function () use ($delim, $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; return $str;

View File

@ -14,6 +14,8 @@ class Ihttp
} }
if(!empty($urlset['query'])) { if(!empty($urlset['query'])) {
$urlset['query'] = "?{$urlset['query']}"; $urlset['query'] = "?{$urlset['query']}";
} else {
$urlset['query'] = '';
} }
if(empty($urlset['port'])) { if(empty($urlset['port'])) {
$urlset['port'] = $urlset['scheme'] == 'https' ? '443' : '80'; $urlset['port'] = $urlset['scheme'] == 'https' ? '443' : '80';

View 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;
}
}

View File

@ -24,7 +24,8 @@ abstract class AbstractData
protected function __construct() 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() public function getTable()
@ -42,22 +43,34 @@ abstract class AbstractData
public static function set($key, $value) public static function set($key, $value)
{ {
if (!self::instance()->table) {
return false;
}
return self::instance()->table->set($key, ['value' => $value]); return self::instance()->table->set($key, ['value' => $value]);
} }
public static function get($key, $default = null) public static function get($key, $default = null)
{ {
if (!self::instance()->table) {
return $default;
}
$data = self::instance()->table->get($key); $data = self::instance()->table->get($key);
return $data ? $data['value'] : $default; return $data ? $data['value'] : $default;
} }
public static function del($key) public static function del($key)
{ {
if (!self::instance()->table) {
return false;
}
return self::instance()->table->del($key); return self::instance()->table->del($key);
} }
public static function exist($key) public static function exist($key)
{ {
if (!self::instance()->table) {
return false;
}
return self::instance()->table->exist($key); return self::instance()->table->exist($key);
} }
@ -70,6 +83,9 @@ abstract class AbstractData
public static function clear() public static function clear()
{ {
if (!self::instance()->table) {
return;
}
foreach (self::instance()->table as $key => $row) { foreach (self::instance()->table as $key => $row) {
self::del($key); self::del($key);
} }
@ -77,6 +93,9 @@ abstract class AbstractData
public static function getAll() public static function getAll()
{ {
if (!self::instance()->table) {
return [];
}
$result = []; $result = [];
foreach (self::instance()->table as $key => $row) { foreach (self::instance()->table as $key => $row) {
$result[$key] = $row['value']; $result[$key] = $row['value'];

View File

@ -17,6 +17,9 @@ class OnlineData extends AbstractData
*/ */
public static function online($userid) public static function online($userid)
{ {
if (!self::instance()->getTable()) {
return 0;
}
$key = "online::" . $userid; $key = "online::" . $userid;
$value = self::instance()->getTable()->incr($key, 'value'); $value = self::instance()->getTable()->incr($key, 'value');
if ($value === 1) { if ($value === 1) {
@ -35,6 +38,9 @@ class OnlineData extends AbstractData
*/ */
public static function offline($userid) public static function offline($userid)
{ {
if (!self::instance()->getTable()) {
return 0;
}
$key = "online::" . $userid; $key = "online::" . $userid;
$value = self::instance()->getTable()->decr($key, 'value'); $value = self::instance()->getTable()->decr($key, 'value');
if ($value === 0) { if ($value === 0) {
@ -57,6 +63,9 @@ class OnlineData extends AbstractData
*/ */
public static function live($userid) public static function live($userid)
{ {
if (!self::instance()->getTable()) {
return 0;
}
$key = "online::" . $userid; $key = "online::" . $userid;
return intval(self::instance()->getTable()->get($key)); return intval(self::instance()->getTable()->get($key));
} }

View File

@ -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);
}
/** /**
* 开始执行任务 * 开始执行任务
*/ */

View File

@ -60,7 +60,7 @@ class LoopTask extends AbstractTask
} }
// 新任务时间、周期 // 新任务时间、周期
if ($task->start_at) { 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->start_at = Carbon::parse($task->loop_at);
$task->end_at = $task->start_at->clone()->addSeconds($diffSecond); $task->end_at = $task->start_at->clone()->addSeconds($diffSecond);
} }

View File

@ -116,6 +116,10 @@ class PushTask extends AbstractTask
if (!Base::isTwoArray($lists)) { if (!Base::isTwoArray($lists)) {
$lists = [$lists]; $lists = [$lists];
} }
// 非 Swoole 运行时artisan/测试)无 swoole 绑定,无法推送,直接跳过(与 AbstractObserver 守卫一致)
if (!app()->bound('swoole')) {
return;
}
$swoole = app('swoole'); $swoole = app('swoole');
foreach ($lists AS $item) { foreach ($lists AS $item) {
if (!is_array($item) || empty($item)) { if (!is_array($item) || empty($item)) {

View File

@ -8,7 +8,7 @@
], ],
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.0", "php": "^8.3",
"ext-curl": "*", "ext-curl": "*",
"ext-dom": "*", "ext-dom": "*",
"ext-ffi": "*", "ext-ffi": "*",
@ -20,40 +20,35 @@
"ext-openssl": "*", "ext-openssl": "*",
"ext-simplexml": "*", "ext-simplexml": "*",
"ext-zip": "*", "ext-zip": "*",
"directorytree/ldaprecord-laravel": "^2.7", "directorytree/ldaprecord-laravel": "^4.0",
"fideloper/proxy": "^4.4.1",
"firebase/php-jwt": "^6.9", "firebase/php-jwt": "^6.9",
"fruitcake/laravel-cors": "^2.0.4", "guanguans/notify": "~1.28.0",
"guanguans/notify": "^1.21.1",
"guzzlehttp/guzzle": "^7.3.0", "guzzlehttp/guzzle": "^7.3.0",
"hedeqiang/umeng": "^2.1", "hedeqiang/umeng": "^2.1",
"laravel/framework": "^v8.83.27", "laravel/framework": "^13.0",
"laravel/tinker": "^v2.6.1", "laravel/tinker": "^3.0",
"laravolt/avatar": "^5.1", "laravolt/avatar": "^6.5",
"league/commonmark": "^2.5", "league/commonmark": "^2.5",
"league/html-to-markdown": "^5.1", "league/html-to-markdown": "^5.1",
"maatwebsite/excel": "^3.1.31", "maatwebsite/excel": "^3.1.69",
"madnest/madzipper": "^v1.1.0",
"matomo/device-detector": "^6.4", "matomo/device-detector": "^6.4",
"mews/captcha": "^3.2.6", "mews/captcha": "^3.5",
"orangehill/iseed": "^3.0.1", "orangehill/iseed": "^3.8",
"overtrue/pinyin": "^4.0", "overtrue/pinyin": "^5.3",
"phpoffice/phppresentation": "^1.1", "phpoffice/phppresentation": "^1.2",
"phpoffice/phpword": "^1.3", "phpoffice/phpword": "^1.4",
"predis/predis": "^1.1.7", "predis/predis": "^2.3",
"smalot/pdfparser": "^2.11", "smalot/pdfparser": "^2.11",
"symfony/mailer": "^6.0" "symfony/console": "^7.4"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-ide-helper": "^v2.10.0", "barryvdh/laravel-ide-helper": "^3.7",
"facade/ignition": "^2.10.2", "fakerphp/faker": "^1.24",
"fakerphp/faker": "^v1.14.1", "hhxsv5/laravel-s": "~3.8.0",
"hhxsv5/laravel-s": "^v3.7.19", "kitloong/laravel-migrations-generator": "^7.4",
"kitloong/laravel-migrations-generator": "^4.4.2", "mockery/mockery": "^1.6",
"laravel/sail": "^v1.8.1", "nunomaduro/collision": "^8.6",
"mockery/mockery": "^1.4.3", "phpunit/phpunit": "^11.5"
"nunomaduro/collision": "^v5.5.0",
"phpunit/phpunit": "^9.5.6"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -95,7 +90,7 @@
"php-http/discovery": true "php-http/discovery": true
} }
}, },
"minimum-stability": "dev", "minimum-stability": "stable",
"prefer-stable": true, "prefer-stable": true,
"repositories": { "repositories": {
} }

5347
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -35,8 +35,9 @@ return [
'port' => env('LDAP_PORT', 389), 'port' => env('LDAP_PORT', 389),
'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'), 'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
'timeout' => env('LDAP_TIMEOUT', 5), 'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', false), // LdapRecord v4use_tls=ldaps沿用旧 LDAP_SSL 变量use_starttls=StartTLS沿用旧 LDAP_TLS 变量)
'use_tls' => env('LDAP_TLS', false), 'use_tls' => env('LDAP_SSL', false),
'use_starttls' => env('LDAP_TLS', false),
], ],
], ],

View File

@ -23,18 +23,19 @@ class UpdateOwnerAddIndexSome20231217 extends Migration
$table->index('project_id'); $table->index('project_id');
$table->index(['project_id','userid']); $table->index(['project_id','userid']);
$table->index('owner'); $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) { Schema::table('project_tasks', function (Blueprint $table) {
$table->index('parent_id'); $table->index('parent_id');
$table->index('dialog_id'); $table->index('dialog_id');
$table->index('userid'); $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) { Schema::table('project_task_users', function (Blueprint $table) {
$table->index(['task_id','userid']); $table->index(['task_id','userid']);
$table->index('owner'); $table->index('owner');
$table->integer('owner')->change(); $table->integer('owner')->nullable()->default(0)->comment('是否任务负责人')->change();
}); });
Schema::table('project_task_files', function (Blueprint $table) { Schema::table('project_task_files', function (Blueprint $table) {
$table->index('project_id'); $table->index('project_id');
@ -63,16 +64,16 @@ class UpdateOwnerAddIndexSome20231217 extends Migration
$table->index('link_id'); $table->index('link_id');
}); });
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) { Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->integer('link')->change(); $table->integer('link')->nullable()->default(0)->comment('是否存在链接')->change();
$table->integer('modify')->change(); $table->integer('modify')->nullable()->default(0)->comment('是否编辑')->change();
$table->integer('forward_show')->change(); $table->integer('forward_show')->nullable()->default(1)->comment('是否显示转发的来源')->change();
}); });
Schema::table('web_socket_dialog_users', function (Blueprint $table) { Schema::table('web_socket_dialog_users', function (Blueprint $table) {
$table->index('dialog_id'); $table->index('dialog_id');
$table->index('userid'); $table->index('userid');
$table->integer('mark_unread')->change(); $table->integer('mark_unread')->nullable()->default(0)->comment('是否标记为未读0否1是')->change();
$table->integer('silence')->change(); $table->integer('silence')->nullable()->default(0)->comment('是否免打扰0否1是')->change();
$table->integer('important')->change(); $table->integer('important')->nullable()->default(0)->comment('是否不可移出(项目、任务、部门人员)')->change();
}); });
Schema::table('web_socket_dialog_msg_todos', function (Blueprint $table) { Schema::table('web_socket_dialog_msg_todos', function (Blueprint $table) {
$table->index('msg_id'); $table->index('msg_id');
@ -80,22 +81,22 @@ class UpdateOwnerAddIndexSome20231217 extends Migration
}); });
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) { Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
$table->index('dialog_id'); $table->index('dialog_id');
$table->integer('mention')->change(); $table->integer('mention')->nullable()->default(0)->comment('是否提及(被@')->change();
$table->integer('silence')->change(); $table->integer('silence')->nullable()->default(0)->comment('是否免打扰0否1是')->change();
$table->integer('email')->change(); $table->integer('email')->nullable()->default(0)->comment('是否发了邮件')->change();
$table->integer('after')->change(); $table->integer('after')->nullable()->default(0)->comment('在阅读之后才添加的记录')->change();
}); });
// 文件相关 // 文件相关
Schema::table('files', function (Blueprint $table) { Schema::table('files', function (Blueprint $table) {
$table->index('pid'); $table->index('pid');
$table->index('cid'); $table->index('cid');
$table->integer('share')->change(); $table->integer('share')->nullable()->default(0)->comment('是否共享')->change();
}); });
Schema::table('file_users', function (Blueprint $table) { Schema::table('file_users', function (Blueprint $table) {
$table->index('file_id'); $table->index('file_id');
$table->index('userid'); $table->index('userid');
$table->integer('permission')->change(); $table->integer('permission')->nullable()->default(0)->comment('权限0只读1读写')->change();
}); });
Schema::table('file_links', function (Blueprint $table) { Schema::table('file_links', function (Blueprint $table) {
$table->index('file_id'); $table->index('file_id');

View File

@ -14,7 +14,8 @@ class UpdateFilesNameLengthTo200 extends Migration
public function up() public function up()
{ {
Schema::table('files', function (Blueprint $table) { 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();
}); });
} }

View File

@ -1,31 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <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">
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd" <testsuites>
bootstrap="vendor/autoload.php" <testsuite name="Unit">
colors="true" <directory suffix="Test.php">./tests/Unit</directory>
> </testsuite>
<testsuites> <testsuite name="Feature">
<testsuite name="Unit"> <directory suffix="Test.php">./tests/Feature</directory>
<directory suffix="Test.php">./tests/Unit</directory> </testsuite>
</testsuite> </testsuites>
<testsuite name="Feature"> <php>
<directory suffix="Test.php">./tests/Feature</directory> <server name="APP_ENV" value="testing"/>
</testsuite> <server name="BCRYPT_ROUNDS" value="4"/>
</testsuites> <server name="CACHE_DRIVER" value="array"/>
<coverage processUncoveredFiles="true"> <!-- <server name="DB_CONNECTION" value="sqlite"/> -->
<include> <!-- <server name="DB_DATABASE" value=":memory:"/> -->
<directory suffix=".php">./app</directory> <server name="MAIL_MAILER" value="array"/>
</include> <server name="QUEUE_CONNECTION" value="sync"/>
</coverage> <server name="SESSION_DRIVER" value="array"/>
<php> <server name="TELESCOPE_ENABLED" value="false"/>
<server name="APP_ENV" value="testing"/> </php>
<server name="BCRYPT_ROUNDS" value="4"/> <source>
<server name="CACHE_DRIVER" value="array"/> <include>
<!-- <server name="DB_CONNECTION" value="sqlite"/> --> <directory suffix=".php">./app</directory>
<!-- <server name="DB_DATABASE" value=":memory:"/> --> </include>
<server name="MAIL_MAILER" value="array"/> </source>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
<server name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit> </phpunit>

View File

@ -3,10 +3,14 @@
namespace Tests\Unit; namespace Tests\Unit;
use App\Models\User; use App\Models\User;
use App\Models\UserDepartment;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase; use Tests\TestCase;
class UserImportParseTest extends TestCase class UserImportParseTest extends TestCase
{ {
use DatabaseTransactions;
public function test_parse_skips_header_and_empty_rows() public function test_parse_skips_header_and_empty_rows()
{ {
$sheet = [ $sheet = [
@ -98,8 +102,14 @@ class UserImportParseTest extends TestCase
// 空/非数组 → 返回空数组 // 空/非数组 → 返回空数组
$this->assertSame([], User::assertValidDepartments([])); $this->assertSame([], User::assertValidDepartments([]));
$this->assertSame([], User::assertValidDepartments('not-array')); $this->assertSame([], User::assertValidDepartments('not-array'));
// 去重 + 转 int + 过滤非正数(这些路径不查库) // 去重 + 转 int + 过滤非正数(存在性校验会查库,需用真实部门 ID
$this->assertSame([3, 5], User::assertValidDepartments(['3', 3, 5, 0, -1])); $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() public function test_assert_valid_departments_rejects_over_limit()