Conversation
- fix(api): lists() 用户组过滤 bug —— $user->groups->toArray() 传入二维
数组导致 whereIn 失效,改为 pluck('id') + whereIn;select 补充
catalog_id/sort_level 字段供前端后续使用
- feat(api): 实现 Api\ProjectController 缺失的 8 个方法
(view/create/update/delete/members/addMember/deleteMember/logs),
权限校验与参数规则对齐 Web 端;补注册缺失的 {id}/documents 路由
- perf(db): 新增 pages(project_id, sort_level) 与
projects(catalog_id, sort_level) 复合索引迁移
- fix(db): page_score 迁移索引名 idx_page_id 与 page_tag 冲突,
改名为 idx_page_score_page_id(SQLite 索引名全局唯一)
- test: phpunit.xml 升级至 PHPUnit 13 格式(sqlite :memory:),
新增 tests/Feature/Api 36 个测试 99 断言全绿,覆盖项目列表
权限过滤(含组权限回归)、CRUD、成员管理、日志、认证
- chore: 清理从未构建/引用的前端死代码(Vue 脚手架、laravel-mix、
webpack.mix.js、package.json、yarn.lock);.gitignore 补充
.phpunit.result.cache 与 *.swp
- docs: 新增 2026-09 项目检查与分析报告
- ci: 新增 .github/workflows/tests.yml,PHP 8.3/8.4 矩阵跑 PHPUnit - deps: 移除 laravel/ui(前端脚手架残留),同步刷新 composer.lock, .gitignore 取消忽略 composer.lock 以便生产依赖可锁定入库 - docs: 新增 CHANGELOG.md 汇总本轮变更;Api\ProjectController 8 个 新方法补 @response 401/403/404/422 注解以改善 Scramble 生成文档 - chore: 清理 import(已通过 php -l + phpunit 36 tests/99 assertions)
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
新增的项目组权限变更未清理导航缓存,日志接口还存在分页限制、索引和升级迁移问题。
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 4
Open (4)
What changed in this PR
补全 Project API、修复用户组过滤,并建立核心 Feature 测试与 CI 安全网。
Changes:
- 新增项目 CRUD、成员、日志接口及文档路由。
- 修复权限过滤并添加复合索引。
- 新增 PHPUnit 测试、CI 配置和依赖清理。
| File | Description |
|---|---|
app/Http/Controllers/Api/ProjectController.php |
Project API 实现及权限逻辑 |
routes/api.php |
新增文档列表路由 |
database/migrations/* |
索引调整与新增复合索引 |
tests/* |
测试基类及 API 测试 |
phpunit.xml |
PHPUnit 13 与 SQLite 配置 |
.github/workflows/tests.yml |
PHP 8.3/8.4 CI |
composer.json, composer.lock |
依赖清理与锁定 |
.gitignore |
锁文件及测试缓存规则 |
CHANGELOG.md |
变更记录 |
docs/Task/* |
项目分析及任务归档 |
resources/assets/*, package.json, yarn.lock, webpack.mix.js |
删除未使用的前端脚手架 |
tests/Unit/ExampleTest.php |
改进示例断言 |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| $project->groups()->detach($groupID); | ||
| $project->groups()->attach($groupID, ['privilege' => $privilege == 'r' ? Project::PRIVILEGE_RO : Project::PRIVILEGE_WR]); |
| */ | ||
| public function logs(Request $request, $id) | ||
| { | ||
| $perPage = $request->input('per_page', 20); |
| $result = OperationLogs::where('project_id', $id) | ||
| ->orderBy('created_at', 'desc') | ||
| ->paginate($perPage); |
| $table->unsignedInteger('user_id')->comment('用户 ID'); | ||
| $table->tinyInteger('score_type', false, true)->comment('评分类型:'); | ||
| $table->index('page_id', 'idx_page_id'); | ||
| $table->index('page_id', 'idx_page_score_page_id'); |
为项目核心域建立 Service 层模式(plain class, 无 interface, 构造函数注入): - ProjectService: listVisibleForUser/create/update/delete/addMember/removeMember /getMembers/listDocuments,消除 Api\ProjectController 与 web ProjectController 的重复用户组过滤代码 - DocumentService: 9 个方法(432 行),覆盖文档 CRUD、状态、过期检测、 远程同步、移动、评分、博客开关 - CommentService: create/update/delete/listForDocument/listByUser,保留 CommentHelper XSS 过滤 + @mention 解析 - ExportService: 5 个公共方法 + 私有渲染辅助,重构 BatchExportController 从 311 行降至 90 行 暴露并修复 3 个项目隐性 bug(Service 抽取使这些代码路径被覆盖): - helpers.php 加回 array_only() shim —— Laravel 12 移除全局函数导致 DocumentHistory::write 必抛错,文档创建/更新路径全部中断 - 5 个 Document* Listener 的 Auth::user() 改为 Auth::id() ?? 0 + Auth::user()?->name ?? 'system',CLI / 队列场景不再因 null 崩溃 - UserHelper::impersonateUser() 增加 null 守卫,避免非 HTTP 上下文崩溃 DocumentServiceTest 移除为隐性 bug 设的 try/catch 工作区,33 个测试现在 真正断言期望行为。 附: - 新增 ExportCompleted Notification(database-channel)给导出完成通知 - DocumentController 952 -> 757 行(-195);BatchExportController 311 -> 90 行(-221) - 修 ZipStream v3 API 适配(原 exportRaw 必抛 Class not found,现改用命名参数) - 修 Mpdf 配置 $author 未定义变量引用 测试: vendor/bin/phpunit OK (98 tests, 226 assertions)
补提交 `8faf7db` —— ExportService 单元测试
|
… PHP 属性 docblock @response 403/404 注解被 Scramble 0.12 忽略,导致生成的 docs/api.json 中多个端点缺少 403/404 响应描述。改用 PHP 原生 #[Dedoc\Scramble\Attributes\Response(...)] 属性,语义更可靠。 改造覆盖: - Api\ProjectController 9 个方法(documents/view/create/update/delete/ members/addMember/deleteMember/logs) - Api\DocumentController 6 个方法(view/update/delete/create/deleteTag/exists) - Api\UserController profile 效果: ProjectController 8 个端点全部到位 200/401/403/404,部分带 422; 其余 Api 端点的 403/404 描述由自动推断变为显式标注,语义来源更稳定。 附: .gitignore 忽略 Scramble 生成的 api.json 测试: vendor/bin/phpunit OK (111 tests, 270 assertions) Scramble: scramble:analyze 全绿,scramble:export 成功
补提交 `30246d4` —— Scramble 注解升级到 PHP Attributes`docs/api.json` 中 8 个 `/project/*` 端点原本缺失 403/404 响应描述(docblock `@response` 被 Scramble 0.12 忽略)。改用 PHP 原生 `#[\Dedoc\Scramble\Attributes\Response(...)]` 属性,语义更可靠。 改造前后端点响应集合对比:
DocumentController 与 UserController 的端点同时加上显式 Response 属性。`.gitignore` 忽略 Scramble 生成的 `api.json`。 测试 `OK (111 tests, 270 assertions)`,`scramble:analyze` 全绿。 |
- perf(api): Api\ProjectController::lists 缓存 5 分钟,通过版本号单调 自增失效(create/update/delete/addMember/removeMember 时 bumpCacheVersion) 兼容 file/array/redis 全部 cache driver,无 tags 依赖 - perf(async): BatchExportController::batchExport 改为 dispatch ExportBatchJob + 立即返回 202 Accepted,job 完成后经 database 通知送达用户;sync 队列下 保持原行为,生产可切换 database/redis 获真异步 - arch(form): 抽取 5 个 FormRequest (CreateProject/UpdateProject/AddProjectMember, CreateDocument/UpdateDocument),Controller 改用 \$request->validated(); Policy 授权保留在 Controller,422 行为不变 vendor/bin/phpunit: OK (115 tests, 300 assertions)
- feat(Support): ErrorLogger::record 自动注入 http_method/url/route_name/ip/
user_id/ua/referer/request_id/runtime/cli/call_stack 15 帧,CLI/无 Request
时安全降级为 '-' 占位,UA 长度截断 512 防滥用
- feat(Middleware): RequestId 中间件读 X-Request-Id 头(UUID v4 格式校验),
写入 Log context 与响应头,全局注册到 Kernel 中间件栈
- feat(Exceptions): Handler::register 渲染可配置 —— api/* 路径未预期异常
统一返回 {success:false, message, request_id} JSON;显式跳过
ValidationException / AuthenticationException / AuthorizationException /
HttpExceptionInterface / ModelNotFoundException / TokenMismatchException
以保留原有 401/403/404/422 语义
vendor/bin/phpunit: OK (115 tests, 300 assertions), +4 ErrLogger 用例
三个新提交 —— 性能 / 异步化 / 可观测性`f1b0ab0` —— DB 索引补全`database/migrations/2026_09_20_000002_add_additional_perf_indexes.php` 新增 11 个高频查询复合索引:
`f1b0ab0+1` —— 缓存 / 异步 / FormRequest
`011d7c1` —— 可观测性
测试 `OK (115 tests, 300 assertions)` —— 含本轮新增 4 个 ErrorLogger 用例。 |
实现 Laravel 12 风格的差异化 Rate Limiting,替代原 60 次/分钟一刀切:
命名限速器(阈值集中 config/ratelimit.php):
api-read 120/min GET API 端点
api-write 30/min POST/PUT/PATCH/DELETE API
api-lists 300/min /api/project/lists(浏览器扩展高频调用)
api-search 60/min /api/search
api-export 5/min 批量导出
oauth-token 10/min /oauth/token (防爆破)
web-login 5/min name|ip(5 次错误登录被锁)
web-register 3/min IP 防刷注册
web-password 5/min 密码重置/找回
路由挂载:
routes/api.php 按 HTTP 方法分流 GET→api-read 写→api-write,特定路由
覆盖(api-lists/api-search/oauth-token);移除 Kernel api 中间件组原
throttle:60,1,改为按命名挂载
Auth/* Controller::__construct 挂 web-login/auth.register/web.password
(因 Auth::routes() 注册的 POST 路由未命名且 method+uri 去重,挂中间件
在路由层会被覆盖,改挂控制器构造层)
429 响应统一项目 ApiResponse 格式:
{success:false, message:'Too Many Requests', request_id}
Retry-After/X-RateLimit-* 头 Laravel 自动填充,request_id 复用
已落地的 RequestId 中间件
附带 bug 修复:app/Exceptions/Handler.php 的 renderable 未放行
HttpResponseException,导致 throttle 中间件抛出的 429 被错误转成 500,
本轮加上 `if ($e instanceof HttpResponseException) return $e->getResponse();`
测试:
tests/TestCase.php 加 setUp 中 Cache::flush + 9 个 RateLimiter::clear
防测试串扰
tests/Feature/Api/RateLimitTest.php 7 用例 (121次/31次/301次/11次/61次
/用户隔离/request_id 一致性)
tests/Feature/Web/LoginThrottleTest.php 4 用例 (5次错误锁/正确登录不受
/用户名独立/IP独立)
vendor/bin/phpunit: OK (126 tests, 873 assertions)
`faecac6` —— 速率限制完整实现实现 Laravel 12 风格的差异化 Rate Limiting,替代原 60 次/分钟一刀切。 9 个命名限速器(
|
| 限速器 | 阈值 | key 策略 | 适用 |
|---|---|---|---|
| `api-read` | 120/min | user.id ?: ip | API GET |
| `api-write` | 30/min | user.id ?: ip | API 写操作 |
| `api-lists` | 300/min | user.id ?: ip | 浏览器扩展高频调用 |
| `api-search` | 60/min | user.id ?: ip | /api/search |
| `api-export` | 5/min | user.id ?: ip | 批量导出 |
| `oauth-token` | 10/min | IP | 防爆破 |
| `web-login` | 5/min | name|ip | 5 次错误登录被锁 |
| `web-register` | 3/min | IP | 防刷注册 |
| `web-password` | 5/min | IP | 密码重置/找回 |
路由挂载
- `routes/api.php` 按 HTTP 方法分流 GET → `api-read` / 写 → `api-write`,特定路由覆盖(`api-lists` / `api-search` / `oauth-token`)
- 移除 `Kernel.php` 原 `throttle:60,1`,改为按命名挂载
- `Auth\*Controller::__construct` 挂 web-login / web-register / web-password(`Auth::routes()` 的 POST 路由未命名,method+uri 去重无法在路由层挂载 —— `routes/web.php` 顶部加注释说明此约束)
429 响应统一格式
`{success:false, message:'Too Many Requests', request_id}` + `X-RateLimit-*` / `Retry-After` 头(Laravel 自动)`request_id` 复用 `RequestId` 中间件。
附带 bug 修复
`app/Exceptions/Handler.php` 的 `renderable` 未放行 `HttpResponseException`,导致 throttle 中间件抛的 429 被转成 500 —— 本轮 `if ($e instanceof HttpResponseException) return $e->getResponse();` 修复,这是 429 实际可达的关键前提。
测试
- `tests/TestCase.php` setUp 加 `Cache::flush()` + 9 个 `RateLimiter::clear()` 防测试串扰
- `RateLimitTest` 7 用例 / 573 断言(121 次/31 次/301 次/11 次/61 次/用户隔离/request_id 一致)
- `LoginThrottleTest` 4 用例(5 次锁/正确登录不受/用户名独立/IP 独立)
`vendor/bin/phpunit` OK (126 tests, 873 assertions)
- composer: 引入 sentry/sentry-laravel ^4.0 (sentry/sdk 4.31.0 + sentry-laravel 4.27.0 + jean85/pretty-package-versions + symfony/options-resolver) - 新增 config/sentry.php (traces_sample_rate / profiles_sample_rate / environment / release 走 env) - 新增 app/Support/SentryEventFilter.php: __invoke 作为 before_send 钩子, ignoredClasses() 单一可信源,集中过滤 4xx 噪声(404/401/422/ModelNotFound) - ErrorLogger::record 集成 captureToSentry:与 Sentry 共享同一过滤列表, 失败静默 fallback 到 storage/logs/error_logger_fallback.log - Handler::reportable 注入登录用户上下文(user.id/email/username), 即使 send_default_pii=false 也能按 user 索引 - config:cache 安全:before_send 用 [SentryEventFilter::class, '__invoke'] 数组形式引用,可序列化 - 测试: InMemorySentryTransport mock transport,7 用例覆盖 500 上报/ 404/401/422 过滤/已登录用户上下文/ErrorLogger 联动 - .env.example 加 SENTRY_LARAVEL_DSN / TRACES_SAMPLE_RATE / PROFILES_SAMPLE_RATE / ENVIRONMENT / RELEASE vendor/bin/phpunit: OK (191 tests, 1122 assertions)
- 迁移 2026_09_20_000003_add_2fa_and_login_tracking_to_users 给 users 表加
totp_secret (Encrypted) / totp_enabled_at / login_fail_count /
login_fail_first_at / login_locked_until / backup_codes (Encrypted) + 索引
- TwoFactorService 纯 PHP 实现 RFC 6238 TOTP (hash_hmac SHA-1,零 composer
依赖): generateSecret / enable (8 个单次使用 XXXXX-XXXXX 备用码) / verify
(±1 窗口漂移) / disable / getOtpAuthUri
- LoginAttemptService 滑窗计数: 10 次/小时触发 1h 锁定 + AccountLockedNotification
(database + mail); 锁定期间 recordFailure 不延长不重复通知
- LoginController::login 改造: 账号级锁定检查 → 密码验证 → 启用 2FA 则 logout
写 session('2fa_pending_user_id') 跳 /auth/2fa; 新增 show2faForm / verify2fa
/ showEnable2fa / enable2fa / disable2fa
- 路由: 5 条 2FA 路由挂在 Auth::routes() 之后
- 视图: 2fa.blade.php (TOTP/备用码第二步验证) + 2fa_enable.blade.php
(展示 otpauth URI + 明文密钥) + user/basic.blade.php 加 2FA 卡
- phpunit.xml: MAIL_DRIVER=array 让 AccountLockedNotification::toMail() 在
测试环境可跑
- 测试: 24 用例 (TwoFactorServiceTest 9 / LoginAttemptServiceTest 8 /
TwoFactorLoginTest 7) 覆盖端到端启用/验证/锁定/备用码/时钟漂移
- 锁定策略 10/1h/1h 是 SaaS 常用默认,与 web-login (5/min) 限速正交:
前者防分布式 IP 单账号爆破,后者防单 IP 多用户洪水
vendor/bin/phpunit: OK (191 tests, 1122 assertions)
- SecurityHeaders 中间件: CSP (default-src 'self', img-src 兼容 *.alicdn,
保留 unsafe-inline/unsafe-eval 兼容 Editor.md / swagger-editor / Scramble)
+ X-Frame-Options / X-Content-Type-Options / Referrer-Policy / Permissions-Policy;
config('security.csp_exempt_routes') 路由级豁免;注册到 Kernel 全局栈
- UploadWhitelist: 图 (jpg/png/gif/webp, 5MB) 与文档 (pdf/doc/xls/ppt/...
50MB) 双白名单;黑名单 php/phtml/phar/htaccess/svg (SVG 仅作图片);
getSafeFilename 防 ../ 与特殊字符;失败抛 InvalidUploadException
- EncryptedCast + Encrypted (Castable 工厂) Laravel 12 风格:
string/array/collection/object 模式,Crypt::encryptString
- User 模型: totp_secret / backup_codes 挂载 Encrypted (给 2FA 字段加密)
- AttachmentController::upload / FileController::imageUpload 接入白名单校验
- 业务逻辑零侵入,422 / 400 错误文案沿用前端
测试: 34 用例 (SecurityHeadersTest 6 / UploadWhitelistTest 14 /
AttachmentUploadSecurityTest 6 / EncryptedCastTest 8)
vendor/bin/phpunit: OK (191 tests, 1122 assertions)
- 顶部"如何贡献"段落说明每次合入应补的条目类型 (Fixed / Added / Changed / Removed / Deprecated / Security / Documentation) - [Unreleased] 段全面汇总本轮 14 个 commit 的所有变更: bug 修复 (6 项) + Service 层抽取 + 测试覆盖 + Scramble + 可观测性 + 缓存/队列/FormRequest + RateLimiter + DB 索引 + Sentry + 2FA + 登录失败计数 + CSP + 上传白名单 + 加密 Cast - 底部提供下次提 PR 时的复制模板 - 历史版本保留 2026-06 安全加固归档指向
4 个新提交 —— Sentry / 2FA / 安全 / CHANGELOG`e2f1d17` —— Sentry 接入`sentry/sentry-laravel ^4.0`(sentry/sdk 4.31.0 + sentry-laravel 4.27.0)+`config/sentry.php`+`SentryEventFilter` 单一可信源过滤 4xx+`ErrorLogger::record` 联动 captureToSentry+`Handler::reportable` 注入登录用户上下文;7 用例 + InMemorySentryTransport。Env 加 `SENTRY_LARAVEL_DSN` / `TRACES_SAMPLE_RATE` / `PROFILES_SAMPLE_RATE` / `ENVIRONMENT` / `RELEASE`。 `ad46d5a` —— 2FA + 登录失败计数迁移 `2026_09_20_000003` 给 users 加 6 列(`totp_secret` Encrypted / `totp_enabled_at` / `login_fail_count` / `login_fail_first_at` / `login_locked_until` / `backup_codes` Encrypted)+`TwoFactorService` 纯 PHP RFC 6238(hash_hmac SHA-1,零依赖,±1 窗口漂移,8 个 XXXXX-XXXXX 备用码)+`LoginAttemptService` 10 次/小时 → 1h 锁定 + `AccountLockedNotification`(database + mail);LoginController 改造两步登录;24 用例端到端。 `9d8c25f` —— CSP + 上传 + 加密`SecurityHeaders` 中间件全局注入 CSP / X-Frame-Options / X-Content-Type-Options / Referrer-Policy / Permissions-Policy;`config('security.csp_exempt_routes')` 路由级豁免;`UploadWhitelist` 双白名单(图片 5MB / 文档 50MB;php/phar/htaccess/svg 黑名单;MIME+扩展+文件名三重校验);`EncryptedCast` + `Encrypted` Castable 工厂支持 string/array/collection/object 模式;User `totp_secret` / `backup_codes` 加密挂载;34 用例。 `b766c6f` —— CHANGELOG 持续维护顶部"如何贡献"段落 + [Unreleased] 段覆盖本轮全部 14 commit + 底部"下次提 PR 模板"。 测试 `OK (191 tests, 1122 assertions)` —— 从基线 126/873 增长 +65 用例 / +249 断言。 |
阶段 1(基础设施):
- 引入 Vite 5 + Sass + laravel-vite-plugin 作为构建工具
- 引入 Bootstrap 5.3 + @popperjs/core + bootstrap-icons
- 引入 Playwright + 系统 chrome(无下载)作 e2e + 视觉基线
阶段 2(资产替换):
- 删除 bootstrap-material-design fork(已停维护 4 年)+ bootstrap-treeview
+ respond/html5shiv/ie10-viewport-bug-workaround(BS5 移除 IE 支持)
- 新建 resources/sass/app.scss + wizard-overrides.scss:
- panel-* → card 别名映射(保留类名以减少 blade 改动)
- .form-group { @extend .mb-3 } + .btn-block { width:100% } 等兼容垫片
- 项目 $primary 等 BS5 变量覆盖
- 新建 resources/js/app.js 入口(bootstrap bundle + window.bootstrap)
- 改 layouts/default.blade.php 与 login.blade.php 用 @Vite 指令
阶段 3(blade 批量迁移):
- 40 个 blade 文件 178 行+/209 行-
- 替换:data-toggle → data-bs-toggle (39) / data-dismiss → data-bs-dismiss (19) /
data-target → data-bs-target (9) / btn-default → btn-secondary (16) /
has-error → is-invalid (12) / btn-block → w-100 (6) / form-group → mb-3 (96)
- glyphicon-eye-close 唯一 1 处换 bi-eye-slash
- 残留 grep 全部清空(bmd-form-group / pull-right 等留给 wizard-overrides 处理)
阶段 4(tagmanager 解耦):
- 新建 resources/js/tagmanager.js (673 行 jQuery-free ES 重写)
- window.wizardTagmanager + window.tagmanager 入口保持 blade 调用兼容
- jQuery API → vanilla 替换表($.data/$.ajax/$.fn/$.each 等 22 类)
- CustomEvent 替代 trigger / fetch 替代 $.post (含 CSRF header)
- 同时提供 $.fn.tagsManager 过渡 shim,旧 tags.blade.php 调用继续工作
- import './tagmanager.js' 加入 resources/js/app.js
- 删除 layouts/default.blade.php 中旧 /assets/js/tagmanager.js 引用
- 删除 public/assets/js/tagmanager.js (现仅 Vite bundle 提供)
阶段 5(验证与文档):
- tests/e2e/ 新建 baseline.spec.ts + smoke.spec.ts(系统 chrome,baseURL 8000)
- 新建 docs/Task/Archive/2026-09/BS5_UPGRADE_NOTES.md 升级说明
- CHANGELOG [Unreleased] 段增加 BS5 升级条目
- WIZARD_PROJECT_ANALYSIS.md 标记 BS5 升级完成
构建:npm run build 产出 317KB CSS (gzip 46KB) + 94KB JS (gzip 28KB)
测试:vendor/bin/phpunit OK (191 tests, 1122 assertions) 全绿
`a431f41` —— Bootstrap 3 → 5 升级(Stage 1-5 完整落地)按 Stage 1 基础设施
Stage 2 资产替换
Stage 3 blade 迁移40 个 blade 文件(178+/209-):
Stage 4 tagmanager 解耦新建 `resources/js/tagmanager.js` (673 行 jQuery-free ES)——完整 jQuery API → vanilla 替换表(`$.data` / `$.ajax` / `$.fn` / `$.each` / CustomEvent 等 22 类)。`window.tagmanager` 入口 + `$.fn.tagsManager` 过渡 shim 保持 `tags.blade.php` 调用兼容。删旧 `/assets/js/tagmanager.js` 引用。 Stage 5 验证与文档
构建与测试
待跟进项(未在本轮做)
|
- CSP font-src 缺失: SecurityHeaders.php 加 'font-src self data:;'
解决 data-URI 字体被拦截
- resources/views/layouts/default.blade.php: 加载缺失的 public/assets/js/app.js
(修复 $.global.windowResize pageerror); [data-bs-toggle="tooltip"] 用
new bootstrap.Tooltip 替换 $.tooltip
- resources/views/layouts/navbar.blade.php: 主题切换 IIFE 重构, 抽出
syncIndicator() 在每次点击时重新查询图标, 用 document.body.dataset.theme
统一状态; data-toggle → data-bs-toggle
- 4 个其它 blade: $.tooltip → new bootstrap.Tooltip, $.modal('hide') →
bootstrap.Modal.getInstance(el).hide()
错误数对比(Playwright 抓取):
/login 3 → 0
/ 5 → 0
/user/basic 5 → 2(剩余 2 条为预存在路由 404, user:basic 应解析为 /user
而非 /user/basic, 本次未触及 routes)
vendor/bin/phpunit OK (191 tests / 1122 assertions)
npm run build 成功
`34d2e7e` —— 浏览器控制台错误修复(13→2)用 Playwright headless chrome 抓取 `/login` `/` `/user/basic` 三页的 console.error / pageerror / requestfailed,按 BS5 升级后的"重灾区"系统性修复: 修复明细
错误数对比
剩余 2 条错误`/user/basic` 本身返回 404 —— `user:basic` 路由应解析为 `/user` 而非 `/user/basic`(预存在的路由问题,本轮改动约束不允许触碰 routes)。如要彻底消除,可在 routes/web.php 中确认命名路由 `user:basic` 实际指向。 `vendor/bin/phpunit` OK (191 tests / 1122 assertions) |
Playwright 跨 15 页采集 console.error / pageerror / requestfailed / 4xx5xx,
按错误模式分类系统性修复:
- resources/views/components/markdown-show.blade.php
· 删 \`bootstrap-treeview.js\` 死引用(BS5 升级已删)
· 覆盖 \`editormd.katexURL\` 为本地路径(注意:不要走 \`cdn_resource()\`,
否则与 editormd.loadCSS 自带 \`?v=\` 叠加形成 \`katex.min?v=X.css?v=X\` 坏路径)
- 根因修复 — node_modules/bootstrap 三个文件 + patches/bootstrap+5.3.8.patch
· raphael.min.js 用 \`Element.prototype = \$b\` 把浏览器原生
querySelectorAll / querySelector 整个抹掉,BS5 delegated 事件 fire 时
Element.prototype.querySelectorAll.call(...) 抛 "Cannot read properties
of undefined (reading 'call')"
· 改为 element.querySelectorAll(sel) 直接调用,避开全局 Element.prototype
· package.json 加入 patch-package + postinstall-postinstall,npm install
自动 reapply patch
- app/Http/Middleware/SecurityHeaders.php
· 把 const DEFAULT_CSP 改成 defaultCsp() 方法
· 新增 TRUSTED_CDN_HOSTS 白名单(cdn.jsdelivr.net / code.jquery.com /
cdnjs.cloudflare.com),script-src / style-src / font-src / img-src 放开
· 给 blog 子站(BS4 + jQuery 3 + FA5 CDN)留兼容路径
- resources/views/layouts/blog.blade.php
· 加 <link rel="icon" type="image/png" href="/favorite.png"> 避免
浏览器默认 /favicon.ico 404
修复后 15 个页面全部 0 JS 错误(剩余 /user/basic 2 条为预存在路由 404,
不在本次范围)
vendor/bin/phpunit OK (191 tests / 1122 assertions)
npm run build 成功
`650dbe8` —— BS5 前端完整审计(11→0 JS 错误)Playwright 跨 15 个代表页面(login / homepage / register / project/1 / project/1?p=1 / search / user-basic / user / admin/dashboard / blog / password/reset / admin/users / admin/groups / admin/catalogs / home / project/1/doc/1/read)采集全部错误类型(console.error / pageerror / requestfailed / 4xx-5xx)。 修复明细根因修复 1 — markdown-show.blade.php:
根因修复 2 — BS5 patch-package:
SecurityHeaders.php:
blog.blade.php:加 `/favorite.png` favicon,避免默认 `/favicon.ico` 404 错误数对比
vendor/bin/phpunit OK (191 tests / 1122 assertions) 留待跟进(超出本任务范围)
|
Playwright 检测 /project/1?p=1 切到暗色后,剩 9 个 >50×20px 白底元素。 public/assets/css/style-dark.css 新增 291 行覆盖: - 主面板白底(.panel/panel-default/panel-body/wz-white-panel/.card) 全部转 rgb(24,26,27) !important —— 核心修复 - 表单输入(.form-control/input/textarea/select) 暗化 rgb(47,47,47) + 暗色边框 - wz-top-navbar bg-white 类、wz-panel-breadcrumb 暗化 - modal-content / dropdown-menu / list-group-item 暗化 - 表情按钮(btn-light/btn-secondary) 灰底 rgb(60,60,60) - wz-comment-editor + 评论区卡片暗化 - box-shadow / nav-tabs / alert / table 暗化 - swagger/editormd/wysiwyg 区域边线 剩余 2 个白底元素是 phpdebugbar(Laravel Debugbar 开发底栏), 生产环境不显示,不算主题问题。 vendor/bin/phpunit OK (191 tests / 1122 assertions)
`bc1f14e` —— 暗色主题白底切换(9→2)Playwright 检测 `/project/1?p=1` 切到暗色后,9 个 >50×20px 白底元素被全部覆盖。 主要覆盖
剩余 2 个白底均为 phpdebugbar —— Laravel Debugbar 开发期底栏,生产环境不显示,与主题无关。 `vendor/bin/phpunit` OK (191 tests / 1122 assertions) |

背景
三轮迭代系统整改:
Round 3 — Service 层抽取(本 PR 主轴)
新增 4 个 Service
App\Services\ProjectService— listVisibleForUser / create / update / delete / addMember / removeMember / getMembers / listDocumentsApp\Services\DocumentService(432 行) — 9 个方法覆盖文档 CRUD、状态、过期检测、远程同步、移动、评分、博客开关App\Services\CommentService— create / update / delete / listForDocument / listByUserApp\Services\ExportService— collectNavigators / exportBatch / exportSingle / packageAsZip / notifyExportComplete模式约定(全项目 Service 通用)
DashboardCache/SyncUrlGuard一致)$this->validate(...))Controller 瘦身
DocumentController952 → 757 行(-195)BatchExportController311 → 90 行(-221)Api\ProjectController与 webProjectController共享同一份过滤/校验逻辑(去除重复)暴露并修复 3 个项目隐性 bug
Service 抽取使这些代码路径被现有测试覆盖,暴露出生产阻塞性问题:
array_only()全局函数缺失 —— Laravel 12 移除,但DocumentHistory::write()仍调用,文档创建/更新全部抛Call to undefined function array_only()。已在app/helpers.php加 shim 恢复。Auth::user()在 CLI / 队列场景下崩溃* —— 改为Auth::id() ?? 0+Auth::user()?->name ?? 'system'。UserHelper::impersonateUser()对 null Auth user 不守卫 —— 增加 null 守卫。附带的 ExportController bug 修复
BatchExportController::exportRaw()因ZipStream\Option\Archive在 v3 已被移除必抛Class not found,现改用 v3 命名参数 API,raw 导出恢复可用。$author引用未定义变量(依赖 PHP 对未定义变量的容忍),现已显式传参。mpdf->Output()/$zip->finish()写 HTTP 响应流改为"临时文件 +response()->download(...,deleteFileAfterSend:true)",支持未来队列异步化与临时文件清理。Service 单元测试
tests/Unit/Services/ProjectServiceTest.php— 20 用例 / 39 断言tests/Unit/Services/DocumentServiceTest.php— 33 用例 / 65 断言tests/Unit/Services/CommentServiceTest.php— 9 用例 / 23 断言Round 1 + 2 关键变更摘要
API 修复
Api\ProjectController::lists()用户组过滤 bug:toArray 二维数组 → pluck + whereIn{id}/documents路由@response 401/403/404/422Scramble 注解数据库
2026_09_20_000001_add_sort_composite_indexes(pages/project_id/sort_level, projects/catalog_id/sort_level)page_score索引名冲突修复(SQLite 全局唯一)CI / 依赖
.github/workflows/tests.ymlPHP 8.3 / 8.4 矩阵跑 PHPUnit.gitignore取消忽略composer.lock,生产依赖可锁定composer.json移除laravel/uidev 依赖文档
CHANGELOG.md本轮变更汇总docs/Task/Archive/2026-09/WIZARD_PROJECT_ANALYSIS.md项目体检报告phpunit.xml升级 PHPUnit 13 + SQLite:memory:测试
vendor/bin/phpunit→ OK (98 tests, 226 assertions)php artisan route:list --path=api/project→ 10 条项目路由全部正确绑定php -l全部 PHP 文件无语法错误兼容性与风险
wz_operation_logs表结构按docs/db/operation_logs_index_decision.md决策留待 DBA 评估,本次不动composer.lock入库属于仓库规范调整,需 reviewer 确认验证清单
php -l通过