wip: article AI polish, category SEO fields, cover generator, membership plan seeder
CI / PHPUnit (PHP 8.3) (push) Failing after 4s
CI / PHPUnit (PHP 8.2) (push) Failing after 1m9s
CI / Deploy (manual gate) (push) Skipped

This commit is contained in:
2026-09-07 18:48:37 +00:00
parent 263b98b218
commit 3cec4c5e18
121 changed files with 4701 additions and 313 deletions
+216 -33
View File
@@ -1,45 +1,228 @@
# 部署与进程管理
# 部署(生产)
## Web
- nginx + php-fpm(或 Laravel Herd
- 参考 `deploy/nginx.conf`
- 附件:生产 `ATTACHMENTS_DRIVER=s3`;开发可用 `local`
Web**nginx + php-fpm**(或同等 PHP-FPM)提供;**不要**把 HTTP 交给 PM2。PM2 只跑队列、调度、Workerman。
## 长驻进程(PM2
配置文件:仓库根目录 `ecosystem.config.cjs`
本地安装见 [install.md](./install.md),迁旧站见 [import.md](./import.md)。
## 架构
```text
浏览器 → nginx → php-fpm → Laravel (public/index.php)
↘ /attachments-local 仅开发;生产走 S3/R2 302
PM2
larablog-queue queue:work 只消费 default
larablog-schedule schedule:work
larablog-ai-workerman workerman:ai 消费 ai-content / ai-moderation
```
`queue:ai` 是开发用的短生命周期 `queue:work` 包装,**不会**起 Workerman。生产用 `workerman:ai` **或** 单独的 `queue:work --queue=ai-content,ai-moderation`,不要和 Workerman **同时**抢同一批 AI Job。
## 服务器
| 项 | 建议 |
|---|---|
| PHP | 8.2 / 8.3 FPM,扩展同安装文档,外加 `redis` |
| Workerman | CLI PHP 需 `pcntl``posix`(与 FPM 不是同一 php.ini 时要两边都查) |
| 数据库 | MySQL 8 / MariaDB 10.6+(不要用 SQLite 当生产) |
| Redis | 队列 + 缓存;多站点共用时设前缀 |
| 附件 | S3 兼容(Cloudflare R2 / 腾讯 COS / 阿里 OSS / MinIO |
| 进程 | Node 的 PM2,或 systemd;配置见仓库根目录 `ecosystem.config.cjs` |
```bash
# 建议 QUEUE_CONNECTION=redis,并先起 Redis
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan themes:publish
php artisan plugins:sync
pm2 start ecosystem.config.cjs
pm2 save
php -m | grep -E 'pcntl|posix|redis|gd'
php-fpm -m | grep -E 'redis|gd'
```
进程:
| name | 作用 |
|---|---|
| `larablog-queue` | `queue:work`default / ai-content / ai-moderation |
| `larablog-schedule` | `schedule:work` |
| `larablog-ai-workerman` | Workerman AI 运行时(可选,若只用 queue:work 可删) |
> HTTP **不要**交给 pm2。
## GitHub Actions
- `.github/workflows/ci.yml`PHP 8.2/8.3 跑 PHPUnitsqlite memory
- `deploy` job 仅作占位,需绑定 `production` environment 与自有发布脚本
## Redis 键前缀
`.env`
## `.env`(生产要点)
```env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://www.example.com
APP_LOCALE=zh_CN
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=larablog
DB_USERNAME=larablog
DB_PASSWORD=
QUEUE_CONNECTION=redis
CACHE_STORE=redis
SESSION_DRIVER=database
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PREFIX=larablog_
CACHE_PREFIX=larablog_cache_
ATTACHMENTS_DRIVER=s3
ATTACHMENTS_DISK=attachments
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=auto
AWS_BUCKET=
AWS_URL=https://cdn.example.com
AWS_ENDPOINT=https://xxx.r2.cloudflarestorage.com
AWS_USE_PATH_STYLE_ENDPOINT=true
AI_PROVIDER=openai_compatible
AI_API_BASE_URL=
AI_API_KEY=
AI_MODEL=
```
Laravel 会在 `config/database.php``redis.options.prefix` 与 cache prefix 生效。
`APP_URL` 必须是对外 origin(含 `https`),否则附件 URL、OG、RSS 会错。
`REDIS_PREFIX` / `CACHE_PREFIX` 避免和同机其他 Laravel 抢键。
## 首次发布
代码放到目标目录后(示例 `/var/www/larablog`):
```bash
cd /var/www/larablog
composer install --no-dev --optimize-autoloader
cp .env.example .env
# 编辑 .env:生产库、Redis、S3、APP_URL、AI_*
php artisan key:generate --force
php artisan migrate --force
php artisan plugins:sync
php artisan migrate --force
php artisan themes:publish
# 新站演示数据(迁 sablog 则跳过,改走 import.md
# php artisan db:seed --force
php artisan db:seed --class=Database\\Seeders\\AiSettingsSeeder --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
chmod -R ug+rwx storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache
```
FPM 用户以发行版为准(`www-data` / `nginx` / `php-fpm`)。
然后配 nginx、起 PM2。
## nginx
示例:`deploy/nginx.conf`。生产至少保证:
- `root` 指向 **`.../public`**,不是仓库根
- `try_files``index.php`
- PHP 走 php-fpm socket(版本与套接字路径改成你机器上的)
- 限制隐藏文件;按需 `client_max_body_size`(后台传附件)
HTTPS 用发行版 certbot / 已有证书终止 TLS。HTTP 仅作跳转。
生产 `ATTACHMENTS_DRIVER=s3` 时,**不必**再配 `location /attachments-local/`。正文与 `/attachment.php?id=` 会 302 到对象存储。
## 附件
| `ATTACHMENTS_DRIVER` | 行为 |
|---|---|
| `local` | 文件在 `storage/app/attachments`URL `/attachments-local/...`(开发) |
| `s3` | Flysystem S3`AWS_*` 指向兼容端点 |
不要把附件长期放在 `public/` 或应用盘当生产方案。导入旧附件见 [import.md](./import.md)。
封面模板字需要中文时,设 `LARABLOG_COVER_FONT` 为服务器上的 TTF/OTF 绝对路径。
## PM2
`ecosystem.config.cjs``cwd` 是仓库根。若部署路径不是开发机路径,把该文件里的约定理解成「在项目根执行 `pm2 start ecosystem.config.cjs`」。
```bash
cd /var/www/larablog
# 如 PHP 不在 PATHPHP_BINARY=/usr/bin/php pm2 start ecosystem.config.cjs
pm2 start ecosystem.config.cjs
pm2 save
pm2 startup
pm2 status
```
| name | 命令 | 日志 |
|---|---|---|
| `larablog-queue` | `queue:work redis --queue=default ...` | `storage/logs/pm2-queue.*.log` |
| `larablog-schedule` | `schedule:work` | `storage/logs/pm2-schedule.*.log` |
| `larablog-ai-workerman` | `workerman:ai start` | `storage/logs/pm2-ai.*.log` |
Workerman 自己的 pid / 日志 / 状态文件在 **`storage/logs/workerman-ai.*`**,不要写到仓库根。若根目录已有 `workerman.log``workerman.artisan.status*`:先停进程再删。
没有 `pcntl`/`posix` 时不要起 `larablog-ai-workerman`,可从 ecosystem 里去掉该 app,改用:
```bash
php artisan queue:work redis --queue=ai-content,ai-moderation --sleep=1 --tries=3
```
调度任务(`routes/console.php`):Redis 时每天 `cache:prune-stale-tags`;每周清理 7 天前的失败队列。
## 插件
```bash
php artisan plugins:sync
# 后台启用,或:
php artisan plugins:sync --enable=larablog/payment,larablog/membership
php artisan migrate --force
php artisan config:cache
```
付费内容依赖支付插件。Stub 支付仅打通下单,不是微信/支付宝。
## 日常更新
```bash
cd /var/www/larablog
git pull
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan plugins:sync
php artisan themes:publish
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan filament:upgrade
pm2 reload ecosystem.config.cjs
```
维护窗口:
```bash
php artisan down
# …发布…
php artisan up
```
`.env` 后必须再 `config:cache`
## 日志与排障
| 文件 | 来源 |
|---|---|
| `storage/logs/laravel.log` | 应用 |
| `storage/logs/pm2-*.log` | PM2 stdout/err |
| `storage/logs/workerman-ai.log` | Workerman |
| `storage/logs/workerman-ai.status` | Workerman 状态(`workerman:ai status` |
```bash
php artisan workerman:ai status
pm2 logs larablog-ai-workerman --lines 100
```
队列堆积:Redis 里看 `REDIS_PREFIX` 下的队列键;确认只开了一种 AI 消费者。
## 发布后自检
- `https://站点/`、一篇 `/show-{id}.shtml``/rss.xml``/sitemap.xml`
- `/admin` 可登录
- 上传一张附件,前台 `/attachment.php?id=` 能跳到对象存储
- 后台触发一次 AI 润色,对应队列被消费(stub 或真实 API)
- `APP_DEBUG=false``.env` 不能从 Web 读到
## CI
`.github/workflows/ci.yml`PHP 8.2/8.3 PHPUnitsqlite memory)。`deploy` job 只是占位,需绑定 GitHub `production` environment 并改成你的 rsync/ssh + 上文更新步骤。
+164
View File
@@ -0,0 +1,164 @@
# 从 sablog 导入
把 SaBlog-X 的内容迁到 LaraBlog**保留文章 / 分类 ID 与内容向 URL**,附件进对象存储(或本地磁盘),源库只读、不改源站文件。
命令:`php artisan sablog:import`
实现:`app/Console/Commands/SablogImportCommand.php`
回归夹具:`tests/fixtures/sablog/``php artisan test --filter=SablogImportTest`
## 迁什么、不迁什么
| 源表(默认前缀 `sablog_` | 结果 |
|---|---|
| `users` | 保留 `userid`;密码写入 `password_legacy`(无盐 MD5);随机 bcrypt 占位;**邮箱为空** |
| `categories` | 保留 `cid` |
| `articles` | 保留 `articleid`、阅读数、置顶、可见、关闭评论、阅读密码 |
| `comments` | `visible=1` → 已通过,否则待审;**不触发**评论插件/AI 审核 |
| `tags` + `aids` | 标签及文章关联 |
| `links` / `stylevars` | 友情链接、站点片段 |
| `attachments` | 元数据 + 按 `--attachments` 目录上传;正文仍存引用,不写死 CDN |
| `trackbacks` / `trackbacklog` / `searchindex` / `sessions` | **跳过**,只在报告里计数 |
不导入旧 PHP 后台、WAP、Trackback。旧 `/admin/*.php` 在新站返回 410;新后台是 `/admin`Filament)。
## 导入前
1. 目标库已 `php artisan migrate`(含插件 migration`AppServiceProvider` 会加载 `plugins/*/database/migrations`)。
2. **不要先跑完整 `db:seed`。** 演示种子占用 `users.id=1``categories.id=1``articles.id=1/2`,导入按同 ID **upsert 覆盖**,还可能留下演示站多出来的行。
3. 角色表是空的:导入**不会**写 Spatie 角色。导入后要自己建 `admin` 并赋给旧站管理员(见文末)。
4. 配好附件盘:开发 `ATTACHMENTS_DRIVER=local`;生产用 S3/R2 等(见 [deploy.md](./deploy.md))。
5. 源库账号建议只读。
## 源库与附件目录
`.env`(连接名默认 `sablog`,见 `config/database.php`):
```env
SABLOG_DB_DRIVER=mysql
SABLOG_DB_HOST=127.0.0.1
SABLOG_DB_PORT=3306
SABLOG_DB_DATABASE=sablog
SABLOG_DB_USERNAME=readonly
SABLOG_DB_PASSWORD=
SABLOG_DB_CHARSET=utf8mb4
```
旧库若是 GBK
```env
SABLOG_DB_CHARSET=gbk
```
命令里再加 `--encoding=gbk``--encoding=auto`(非合法 UTF-8 时按 GBK 转)。
附件目录必须是**绝对路径**,且相对路径与表字段 `filepath` 一致。例如库里是 `2020/01/foo.jpg`,则文件应在:
```text
/data/sablog/attachments/2020/01/foo.jpg
```
缩略图字段 `thumb_filepath` 同样按该根目录拼接;存在则一并上传。
## 两种正文模式
| `--mode` | 库内正文 | `content_format` | 附件引用 |
|---|---|---|---|
| `raw`(推荐先跑) | 保持 HTML | `html` | 原样 `[attach=123]` |
| `markdown` | HTML → Markdown | `markdown` | 改成 `attach:123`(避免 `[]` 被 Markdown 吃掉) |
两种模式都**不把对象存储 URL 写进正文**。前台渲染再变成 `/attachment.php?id=123`
不确定旧文 HTML 质量时先 `raw`,确认站点可访问后再决定是否用 `markdown` 重导(同 ID upsert,可重跑)。
## 命令
先连通、只计数(不写目标库、不上传):
```bash
php artisan sablog:import --mode=raw --dry-run
```
正式导入:
```bash
php artisan sablog:import \
--mode=raw \
--connection=sablog \
--prefix=sablog_ \
--attachments=/绝对路径/sablog/attachments \
--disk=attachments \
--encoding=auto
```
| 参数 | 默认 | 说明 |
|---|---|---|
| `--connection` | `sablog` | Laravel 连接名 |
| `--prefix` | `sablog_` | 源表前缀 |
| `--attachments` | 空 | 本地附件根目录;不传则附件记为 missing |
| `--mode` | `raw` | `raw` \| `markdown` |
| `--encoding` | `auto` | `utf8` \| `gbk` \| `auto` |
| `--disk` | `attachments` | 目标 disk |
| `--retry-failed` | 关 | 已有 `synced_at` 的附件默认跳过;打开则重试失败/缺失 |
| `--dry-run` | 关 | 只读源库并打印计数 |
成功后打印一张表:`users` / `categories` / `articles` / `comments` / `tags` / `links` / `stylevars` / `attachments_ok` / `attachments_missing` / `attachments_failed` / 跳过的 trackback 等。源库与源文件不会被修改。
整次导入包在一个事务里(附件上传在事务内;失败会 warn 并继续记 missing/failed 行)。缺文件不会中断整次导入。
## 附件补传
第一次没带 `--attachments`、或路径不对导致 `attachments_missing`
```bash
php artisan sablog:import --mode=raw --attachments=/正确/路径 --retry-failed
```
已成功(`synced_at` 有值)的附件默认不再上传。
## 导入后:登录与后台
旧站密码是 **MD5**。导入后:
- 前台 `/login.shtml` 用**用户名 + 旧密码**可登录;校验成功后升级为 bcrypt,并清空 `password_legacy`
- Filament `/admin` 用**邮箱**登录。导入用户 `email` 为空,且**没有** `admin` 角色,所以要补一步:
```bash
php artisan tinker
```
```php
use App\Models\User;
use Spatie\Permission\Models\Role;
Role::findOrCreate('admin');
Role::findOrCreate('editor');
Role::findOrCreate('member');
$user = User::query()->where('username', '旧站管理员用户名')->first();
$user->forceFill(['email' => 'you@example.com'])->save();
$user->assignRole('admin');
```
然后打开 `/admin`,邮箱 + **旧站密码**(或升级后的同一密码)。
AI 设置可单独灌,不必跑演示博客种子:
```bash
php artisan db:seed --class=Database\\Seeders\\AiSettingsSeeder
```
## 导入后检查
- 首页、`/show-{旧文章id}.shtml``/category-{旧分类id}.shtml`
- 正文图是否经 `/attachment.php?id=` 能打开
- 评论作者、友情链接、站点片段
- `attachments_missing` / `attachments_failed` 是否可接受
- 阅读密码文章:前台仍走密码墙(与单篇付费、会员可见互斥,见后台校验)
## 注意
- **幂等**:按原 ID upsert,可重复执行;附件成功过的默认跳过。
- **ID 对齐是为了 SEO**:不要在导入后再批量改文章 ID。
- 导入用户没有邮箱、没有角色;前台读者用用户名登录即可。
- 插件付费/会员数据不在 sablog 里,导入后如需付费墙,在新后台按篇配置。
- 夹具库仅供测试,不是完整 sablog 结构说明;以你线上表为准。缺表则该项计数为 0,不报错退出。
+156
View File
@@ -0,0 +1,156 @@
# 安装(本地开发)
把 LaraBlog 跑起来:前台、Filament 后台、演示数据。从 sablog 迁站请先看 [import.md](./import.md),**不要先灌演示数据**。上线见 [deploy.md](./deploy.md)。
## 环境
| 项 | 要求 |
|---|---|
| PHP | **8.2+**`composer.json` |
| 扩展 | `mbstring``openssl``pdo``tokenizer``xml``ctype``json``fileinfo``gd`(封面/图片)、`zip` |
| Composer | 2.x |
| 数据库 | 开发可用 SQLite;也可用 MySQL / MariaDB |
| 可选 | Redis(队列/缓存);Node.js 仅在改前端资源时需要 |
| 可选 | `pcntl` + `posix`(本机跑 `workerman:ai`;没有就用 `queue:ai` |
macOS 可用 Laravel Herd / Valet,指向仓库的 `public/`。也可用 `php artisan serve`
## 1. 代码与依赖
```bash
cd /path/to/larablog
composer install
cp .env.example .env
php artisan key:generate
```
## 2. 配置 `.env`
最少改这些:
```env
APP_NAME=LaraBlog
APP_ENV=local
APP_DEBUG=true
APP_URL=http://larablog.test # 必须和浏览器访问的 origin 一致
APP_LOCALE=zh_CN
# 开发默认 SQLite(文件需存在)
DB_CONNECTION=sqlite
# DB_DATABASE=/绝对路径/database/database.sqlite
# 开发附件落本地,不必配 S3
ATTACHMENTS_DRIVER=local
QUEUE_CONNECTION=database
CACHE_STORE=database
SESSION_DRIVER=database
# 本机没有真实 LLM 时用 stub,避免后台点「AI 润色」去打外网
AI_PROVIDER=stub
```
SQLite 文件:
```bash
mkdir -p database
touch database/database.sqlite
```
改用 MySQL 时:
```env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=larablog
DB_USERNAME=root
DB_PASSWORD=
```
## 3. 初始化
```bash
php artisan migrate
php artisan db:seed
php artisan plugins:sync
php artisan migrate # 插件表(支付/会员等)随 discover 加载,再跑一遍即可
php artisan themes:publish
```
`db:seed` 会:
- 写入 AI 设置(来自 `.env``AI_*`
- 启用 `larablog/ai-comment-moderation`,并尝试启用 `payment` / `membership`
- 创建演示分类、文章、友情链接
- 创建后台账号
## 4. 启动
**Herd / nginx** 站点根目录设为 `public/`,打开 `APP_URL`
**内置服务器:**
```bash
php artisan serve
```
本地要排空队列(含 AI)可以另开终端:
```bash
php artisan queue:work --tries=3
# 或只处理 AI 队列(不会启动 Workerman):
php artisan queue:ai
```
改 Filament/Vite 资源时才需要 `npm install && npm run dev`。主题 CSS 走 `php artisan themes:publish` 拷到 `public/themes/`
## 5. 入口与账号
| 入口 | 地址 |
|---|---|
| 前台 | `/``/show-1.shtml``/login.shtml` |
| 后台 | `/admin` |
| API | `/api/v1/meta``/api/v1/articles` |
| OpenAPI | `/docs/api/openapi.yaml` |
演示管理员(`DemoBlogSeeder`):
- 邮箱:`admin@larablog.test`
- 密码:`password`
登录后请立刻改密码。不要把这组账号用在公网。
## 6. 插件(可选)
```bash
php artisan plugins:sync
# 后台 → 插件 → 启用;或:
php artisan plugins:sync --enable=larablog/payment,larablog/membership
php artisan migrate
```
依赖顺序:先 `larablog/payment`,再 `larablog/paid-content` / `larablog/membership`。说明见后台卡片「使用说明」,以及 `docs/plugins.md`
## 7. 自检
```bash
php artisan test
php artisan route:list --path=shtml
```
浏览器打开首页、一篇 `.shtml` 文章、`/admin`。本地附件地址形如 `/attachments-local/...`(由 Laravel 路由提供,不必 `storage:link`)。
## 常见问题
**500 / 空白页**
`storage/``bootstrap/cache/` 对 PHP 进程可写;`APP_KEY` 已生成;`APP_DEBUG=true``storage/logs/laravel.log`
**后台样式丢失**
执行 `php artisan filament:upgrade``php artisan themes:publish`
**点了 AI 润色没反应**
`AI_PROVIDER=stub` 时也要有队列消费者;`QUEUE_CONNECTION=sync` 会在请求内执行(仅调试)。生产见 [deploy.md](./deploy.md)。
**准备导入旧站**
不要用这份「演示种子」当生产数据。空库 `migrate` 后直接走 [import.md](./import.md)。