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-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->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());
}
}

View File

@ -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) {
}
//

View File

@ -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) {
}
//

View File

@ -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) {
}
//

View File

@ -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(),
];

View File

@ -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),
]);
}
/**

View File

@ -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,

View File

@ -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

View File

@ -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',
];

View File

@ -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 = [];
/**

View File

@ -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',
];
/**

View File

@ -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(),
]

View File

@ -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();

View File

@ -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 机器人设置

View File

@ -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

View File

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

View File

@ -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;

View File

@ -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';

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()
{
$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'];

View File

@ -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));
}

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) {
$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);
}

View File

@ -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)) {

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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 v4use_tls=ldaps沿用旧 LDAP_SSL 变量use_starttls=StartTLS沿用旧 LDAP_TLS 变量)
'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','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');

View File

@ -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();
});
}

View File

@ -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>

View File

@ -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()