# 从 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，不报错退出。
