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
+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,不报错退出。