# 部署（生产）

Web 由 **nginx + php-fpm**（或同等 PHP-FPM）提供；**不要**把 HTTP 交给 PM2。PM2 只跑队列、调度、Workerman。

本地安装见 [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
php -m | grep -E 'pcntl|posix|redis|gd'
php-fpm -m | grep -E 'redis|gd'
```

## `.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=
```

`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 不在 PATH：PHP_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 PHPUnit（sqlite memory）。`deploy` job 只是占位，需绑定 GitHub `production` environment 并改成你的 rsync/ssh + 上文更新步骤。
