# 部署指南

## 环境变量（.env 关键项）

```dotenv
APP_URL=https://blog.example.com
APP_LOCALE=zh_CN

# MySQL 8
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=laralog
DB_USERNAME=laralog
DB_PASSWORD=***

# S3 兼容附件（R2 / COS / OSS）
S3_ACCESS_KEY=xxx
S3_SECRET_KEY=xxx
S3_REGION=auto                # R2 用 auto；COS/OSS 填对应区域
S3_BUCKET=laralog
S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com   # COS: https://cos.<region>.myqcloud.com / OSS: https://oss-<region>.aliyuncs.com
S3_URL=https://media.example.com                          # 公开访问域名（CDN），留空则用 endpoint+bucket 拼
S3_VISIBILITY=public
UPLOAD_DISK=uploads

# LLM（OpenAI 兼容：DeepSeek / 通义 / 自建代理）
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_API_KEY=sk-xxx
LLM_MODEL=deepseek-chat

# Workerman
WORKERMAN_QUEUE_WORKERS=2
WORKERMAN_WS_ENABLED=true
WORKERMAN_WS_PORT=8787

# 插件/主题市场（留空仅支持本地 ZIP）
MARKET_URL=
MARKET_TOKEN=
```

## 生产迁移顺序

1. `php artisan migrate --seed`
2. 导入老数据：`php artisan sablog:import --database=sablog ... --attachments-dir=... --sync-attachments --convert-markdown`
3. 配置 S3 后同步附件：`php artisan attachments:sync-s3 --legacy-dir=...`
4. 生成主题资产（可选）：`php artisan theme:publish`
5. 启动 Workerman + 队列 + 调度器

## Nginx（前端由 Laravel 接管，老 URL 由应用层 301）

```nginx
server {
    listen 443 ssl http2;
    server_name blog.example.com;

    root /var/www/laralog/public;
    index index.php;

    ssl_certificate     /etc/nginx/ssl/blog.crt;
    ssl_certificate_key /etc/nginx/ssl/blog.key;

    # 主题资产（生产已 theme:publish 时可交给 Nginx；未发布则走 Laravel 路由）
    location ~ ^/themes/.+\.(css|js|png|jpg|jpeg|gif|svg|woff2?|ico)$ {
        try_files $uri /index.php?$query_string;
        expires 7d;
        add_header Cache-Control "public";
    }

    # 附件已全部走 S3，无需本地静态附件目录

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~ /\.(?!well-known).* { deny all; }
}
```

> 兼容性说明：所有 sablog 老 URL（`/show-12.html`、`/?action=show&id=12`、`rss.php`、`attachment.php` 等）均由 `routes/web.php` 处理为 301，无需在 Nginx 额外配置重写。

## systemd：Workerman 常驻服务

```ini
# /etc/systemd/system/laralog-workerman.service
[Unit]
Description=LaraLog Workerman (queue + websocket)
After=network.target mysql.service

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/laralog
ExecStart=/usr/bin/php artisan workerman:serve start
ExecStop=/usr/bin/php artisan workerman:serve stop
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
```

```bash
systemctl enable --now laralog-workerman
```

## 调度器（cron）

```cron
* * * * * cd /var/www/laralog && php artisan schedule:run >> /dev/null 2>&1
```

调度器内注册的内容：`spatie/laravel-backup` 备份、缓存清理等（按需在 `routes/console.php` 增删）。

## 队列消费：workerman 与 queue:work 等价

队列任务（AI 审核/润色等）由**消费者**执行，两种方式消费同一个队列、功能完全等价：

| | workerman:serve | queue:work |
|---|---|---|
| 执行任务 | ✅ | ✅ |
| 性能 | 常驻进程，快 | 每任务启动框架，慢 |
| WebSocket 进度推送 | ✅ :8787 | ❌（任务照常执行，仅后台无实时提示） |
| 适用 | 生产推荐 | 降级 / 标准 Laravel 部署 |

**两者不要同时跑同一个队列**（虽不会重复消费，但浪费资源）。改业务代码后两者都需要重启才生效（常驻内存）；`queue:work --once` 每任务新进程，适合调试。

### 降级方案（不用 Workerman）

```ini
# /etc/systemd/system/laralog-queue.service
[Service]
User=www-data
WorkingDirectory=/var/www/laralog
ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 --queue=default,ai
Restart=always
```

### 开发期热重载

```bash
php artisan dev:watch   # 监听 app/plugins/routes/config/.env 变化，自动重启 workerman（fswatch 或 PHP 轮询兜底）
```

## 备份

```bash
# spatie/laravel-backup（备份 DB + 附件；S3 附件无需本地备份）
php artisan backup:run --only-db
```

## 常见操作

```bash
php artisan theme:publish          # 把主题 assets 拷贝到 public/themes
php artisan workerman:serve status # 查看常驻进程
php artisan content:convert --all  # 老 HTML 文章批量转 Markdown
```
