# 宫中有喜 — 部署方案

## 一、部署架构

```
┌─────────────────────────────────────────┐
│              CDN (可选)                   │
│         静态资源加速                      │
└──────────────────┬──────────────────────┘
                   │
┌──────────────────▼──────────────────────┐
│          Nginx (反向代理)                 │
│   80/443 → 静态资源 / API / WebSocket    │
└────┬──────────────┬─────────────────────┘
     │              │
     ▼              ▼
┌─────────┐  ┌──────────────┐
│ Vue SPA │  │  PHP-FPM      │
│ (静态)   │  │  Laravel App  │
└─────────┘  └──────┬───────┘
                    │
        ┌───────────┼───────────┐
        ▼           ▼           ▼
  ┌──────────┐ ┌────────┐ ┌─────────┐
  │ MySQL 8  │ │ Redis  │ │ Storage │
  │          │ │        │ │ (文件)   │
  └──────────┘ └────────┘ └─────────┘
```

---

## 二、环境要求

| 组件 | 最低版本 | 推荐 |
|------|----------|------|
| OS | Ubuntu 20.04 / CentOS 8 | Ubuntu 22.04 |
| PHP | 8.1 | 8.2 |
| MySQL | 8.0 | 8.0 |
| Redis | 6.0 | 7.0 |
| Nginx | 1.18 | 1.24 |
| Node.js | 16 | 18 LTS |
| Composer | 2.x | 2.x |
| Docker | 20.10 | 24.x |
| Docker Compose | 2.x | 2.x |

### PHP 扩展

```
php-fpm php-mysql php-redis php-mbstring php-xml
php-curl php-gd php-zip php-bcmath php-intl
php-opcache php-fileinfo
```

---

## 三、Docker Compose 部署

### 3.1 docker-compose.yml

```yaml
version: '3.8'

services:
  # Nginx 反向代理
  nginx:
    image: nginx:1.24-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/nginx/conf.d:/etc/nginx/conf.d
      - ./docker/nginx/ssl:/etc/nginx/ssl
      - ./frontend/dist:/var/www/frontend
      - ./backend/public:/var/www/backend/public
      - ./backend/storage/app/public:/var/www/backend/storage/app/public
    depends_on:
      - php
    networks:
      - app-network
    restart: unless-stopped

  # PHP-FPM
  php:
    build:
      context: ./docker/php
      dockerfile: Dockerfile
    volumes:
      - ./backend:/var/www/backend
    depends_on:
      - mysql
      - redis
    networks:
      - app-network
    restart: unless-stopped

  # Laravel 队列 Worker
  queue:
    build:
      context: ./docker/php
      dockerfile: Dockerfile
    command: php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
    volumes:
      - ./backend:/var/www/backend
    depends_on:
      - mysql
      - redis
    networks:
      - app-network
    restart: unless-stopped

  # Laravel 定时任务
  scheduler:
    build:
      context: ./docker/php
      dockerfile: Dockerfile
    command: /bin/sh -c "while true; do php /var/www/backend/artisan schedule:run --verbose --no-interaction >> /dev/null 2>&1; sleep 60; done"
    volumes:
      - ./backend:/var/www/backend
    depends_on:
      - mysql
      - redis
    networks:
      - app-network
    restart: unless-stopped

  # MySQL
  mysql:
    image: mysql:8.0
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - mysql-data:/var/lib/mysql
      - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./docker/mysql/init:/docker-entrypoint-initdb.d
    networks:
      - app-network
    restart: unless-stopped

  # Redis
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis-data:/data
    networks:
      - app-network
    restart: unless-stopped

volumes:
  mysql-data:
  redis-data:

networks:
  app-network:
    driver: bridge
```

### 3.2 PHP Dockerfile

```dockerfile
# docker/php/Dockerfile
FROM php:8.2-fpm-alpine

# 安装扩展
RUN apk add --no-cache \
    libpng-dev libjpeg-turbo-dev freetype-dev \
    libzip-dev icu-dev oniguruma-dev \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install \
    pdo_mysql mbstring xml curl gd zip bcmath intl opcache pcntl \
    && pecl install redis \
    && docker-php-ext-enable redis

# Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# PHP 配置
COPY php.ini /usr/local/etc/php/conf.d/custom.ini

WORKDIR /var/www/backend

# 权限
RUN chown -R www-data:www-data /var/www/backend

USER www-data

EXPOSE 9000
CMD ["php-fpm"]
```

### 3.3 PHP 配置

```ini
; docker/php/php.ini
upload_max_filesize = 20M
post_max_size = 25M
memory_limit = 256M
max_execution_time = 60
date.timezone = Asia/Shanghai

; OPcache
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.revalidate_freq=0
```

### 3.4 Nginx 配置

```nginx
# docker/nginx/conf.d/default.conf

# 后台管理 SPA
server {
    listen 80;
    server_name admin.example.com;

    root /var/www/frontend;
    index index.html;

    # SPA 路由
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 静态资源缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # Gzip
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml;
    gzip_min_length 1024;
}

# API 服务
server {
    listen 80;
    server_name api.example.com;

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

    # Laravel 路由
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP-FPM
    location ~ \.php$ {
        fastcgi_pass php:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 60;
    }

    # 文件上传大小
    client_max_body_size 25m;

    # 静态资源
    location /storage {
        alias /var/www/backend/storage/app/public;
        expires 30d;
    }

    # 禁止访问隐藏文件
    location ~ /\. {
        deny all;
    }
}
```

### 3.5 MySQL 配置

```ini
# docker/mysql/my.cnf
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
default-time-zone = '+08:00'

# InnoDB
innodb_buffer_pool_size = 512M
innodb_log_file_size = 128M
innodb_flush_log_at_trx_commit = 2

# 连接
max_connections = 200
wait_timeout = 600

# 慢查询
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2

# 二进制日志（用于备份恢复）
log-bin = mysql-bin
binlog_expire_logs_seconds = 604800
binlog_format = ROW

[client]
default-character-set = utf8mb4
```

---

## 四、环境变量 (.env)

```bash
# .env (生产环境示例)
APP_NAME="宫中有喜"
APP_ENV=production
APP_KEY=base64:xxxx
APP_DEBUG=false
APP_URL=https://api.example.com

# 数据库
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=gongzhongyouxi
DB_USERNAME=appuser
DB_PASSWORD=strong_password_here
DB_ROOT_PASSWORD=root_strong_password

# Redis
REDIS_HOST=redis
REDIS_PASSWORD=redis_password
REDIS_PORT=6379

# 缓存/队列/会话
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis

# 文件存储
FILESYSTEM_DISK=local
# 或 OSS:
# FILESYSTEM_DISK=oss
# OSS_ACCESS_KEY_ID=xxx
# OSS_ACCESS_KEY_SECRET=xxx
# OSS_BUCKET=xxx
# OSS_ENDPOINT=xxx

# 微信小程序
WECHAT_MINI_APP_ID=wxXXXXXXXXXXX
WECHAT_MINI_SECRET=xxxxxxxxxx

# 微信支付
WECHAT_PAY_MCH_ID=xxxxxxxxxx
WECHAT_PAY_KEY=xxxxxxxxxx
WECHAT_PAY_CERT_PATH=/path/to/cert
WECHAT_PAY_KEY_PATH=/path/to/key
WECHAT_PAY_NOTIFY_URL=https://api.example.com/api/v1/client/pay/callback

# 短信
SMS_DRIVER=aliyun
SMS_KEY=xxx
SMS_SECRET=xxx
SMS_SIGN=宫中有喜

# 日志
LOG_CHANNEL=daily
LOG_LEVEL=warning
```

---

## 五、部署流程

### 5.1 首次部署

```bash
# 1. 克隆代码
git clone <repo> /opt/saas
cd /opt/saas

# 2. 配置环境变量
cp .env.example .env
vim .env  # 填入生产配置

# 3. 构建前端
cd frontend
npm install
npm run build
cd ..

# 4. 启动容器
docker compose up -d

# 5. 初始化 Laravel
docker compose exec php composer install --no-dev --optimize-autoloader
docker compose exec php php artisan key:generate
docker compose exec php php artisan migrate --force
docker compose exec php php artisan db:seed --class=InitSeeder
docker compose exec php php artisan storage:link
docker compose exec php php artisan config:cache
docker compose exec php php artisan route:cache
docker compose exec php php artisan view:cache

# 6. 验证
curl https://api.example.com/api/v1/health
```

### 5.2 更新部署

```bash
# 1. 拉取代码
cd /opt/saas
git pull origin main

# 2. 更新后端依赖
docker compose exec php composer install --no-dev --optimize-autoloader

# 3. 数据库迁移
docker compose exec php php artisan migrate --force

# 4. 清除缓存
docker compose exec php php artisan config:cache
docker compose exec php php artisan route:cache
docker compose exec php php artisan view:cache
docker compose exec php php artisan queue:restart

# 5. 更新前端（如有变更）
cd frontend
npm install
npm run build
cd ..

# 6. 重载 Nginx
docker compose exec nginx nginx -s reload
```

### 5.3 零停机部署（可选）

```bash
# 使用 Laravel 维护模式
docker compose exec php php artisan down --retry=60

# 执行部署步骤...

docker compose exec php php artisan up
```

---

## 六、备份策略

### 6.1 数据库备份

```bash
#!/bin/bash
# scripts/backup-db.sh
BACKUP_DIR="/opt/backups/mysql"
DATE=$(date +%Y%m%d_%H%M)
KEEP_DAYS=30

mkdir -p $BACKUP_DIR

# 备份
docker compose exec -T mysql mysqldump \
    -u root -p"${DB_ROOT_PASSWORD}" \
    --single-transaction \
    --routines --triggers \
    ${DB_DATABASE} | gzip > "${BACKUP_DIR}/db_${DATE}.sql.gz"

# 清理旧备份
find $BACKUP_DIR -name "*.sql.gz" -mtime +$KEEP_DAYS -delete

echo "[$(date)] Backup completed: db_${DATE}.sql.gz"
```

### 6.2 文件备份

```bash
#!/bin/bash
# scripts/backup-files.sh
BACKUP_DIR="/opt/backups/files"
DATE=$(date +%Y%m%d)

mkdir -p $BACKUP_DIR

# 备份上传文件
tar -czf "${BACKUP_DIR}/storage_${DATE}.tar.gz" \
    -C /opt/saas/backend storage/app/public

# 清理 30 天前
find $BACKUP_DIR -name "*.tar.gz" -mtime +30 -delete
```

### 6.3 定时任务

```bash
# crontab -e
# 每天凌晨 3 点备份数据库
0 3 * * * /opt/saas/scripts/backup-db.sh >> /var/log/backup.log 2>&1
# 每周日凌晨 4 点备份文件
0 4 * * 0 /opt/saas/scripts/backup-files.sh >> /var/log/backup.log 2>&1
```

---

## 七、监控与日志

### 7.1 健康检查

```php
// routes/api.php
Route::get('/health', function () {
    try {
        DB::connection()->getPdo();
        Redis::ping();
        return response()->json([
            'status' => 'healthy',
            'timestamp' => now()->toISOString(),
            'services' => [
                'database' => 'ok',
                'redis' => 'ok',
                'storage' => is_writable(storage_path()) ? 'ok' : 'error',
            ]
        ]);
    } catch (\Exception $e) {
        return response()->json(['status' => 'unhealthy', 'error' => $e->getMessage()], 500);
    }
});
```

### 7.2 日志管理

```php
// config/logging.php — 生产配置
'channels' => [
    'daily' => [
        'driver' => 'daily',
        'path' => storage_path('logs/laravel.log'),
        'level' => 'warning',
        'days' => 14,
    ],
    'business' => [
        'driver' => 'daily',
        'path' => storage_path('logs/business.log'),
        'level' => 'info',
        'days' => 30,
    ],
],
```

### 7.3 监控检查清单

| 监控项 | 方法 | 告警条件 |
|--------|------|----------|
| 服务存活 | /health 接口 | 连续 3 次失败 |
| CPU 使用率 | 系统监控 | > 80% 持续 5 分钟 |
| 内存使用率 | 系统监控 | > 85% |
| 磁盘使用率 | 系统监控 | > 80% |
| MySQL 连接数 | SHOW STATUS | > 150 |
| MySQL 慢查询 | 慢查询日志 | > 10 条/小时 |
| Redis 内存 | INFO memory | > 80% maxmemory |
| 队列积压 | queue:monitor | > 100 条待处理 |
| Laravel 日志 | 日志文件 | ERROR 级别 |
| 响应时间 | Nginx access_log | P95 > 2s |
| SSL 证书 | 定期检查 | 到期前 30 天 |

---

## 八、安全加固

### 8.1 服务器

```bash
# 防火墙 — 仅开放必要端口
ufw allow 22/tcp    # SSH
ufw allow 80/tcp    # HTTP
ufw allow 443/tcp   # HTTPS
ufw enable

# SSH 加固
# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
```

### 8.2 Docker 安全

- 不使用 root 运行容器（PHP Dockerfile 已配置 www-data）
- MySQL/Redis 端口不暴露到公网（仅限 Docker 内部网络）
- 定期更新基础镜像
- 使用 `.dockerignore` 排除敏感文件

### 8.3 应用安全

| 措施 | 配置 |
|------|------|
| HTTPS 强制 | Nginx redirect 80→443 |
| CORS | 限定域名白名单 |
| CSRF | Sanctum SPA 模式自动处理 |
| Rate Limiting | Laravel throttle 中间件 |
| SQL 注入 | Eloquent 参数绑定 |
| XSS | HTMLPurifier + CSP Header |
| 文件上传 | 白名单 + 大小限制 + 病毒扫描 |
| 敏感数据 | .env 不入 Git，密钥加密存储 |

### 8.4 SSL 证书

```bash
# Let's Encrypt 免费证书
apt install certbot python3-certbot-nginx
certbot --nginx -d admin.example.com -d api.example.com

# 自动续期
certbot renew --dry-run
# crontab
0 0 1 */2 * certbot renew --quiet
```

---

## 九、性能优化

### 9.1 Laravel 优化

```bash
# 配置/路由/视图缓存
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Composer 自动加载优化
composer install --optimize-autoloader --no-dev
```

### 9.2 MySQL 优化

- 索引覆盖常用查询（见 02-database-design.md 索引策略）
- 开启查询缓存（Redis 层面）
- 大表分页使用 cursor 分页
- 定期 ANALYZE TABLE

### 9.3 Redis 缓存策略

| 缓存键 | TTL | 场景 |
|--------|-----|------|
| user:{id} | 30min | 用户信息 |
| menu:{role_id} | 60min | 角色菜单 |
| room:board:{store_id}:{date} | 5min | 房态看板 |
| config:{key} | 60min | 系统配置 |
| dict:{type} | 24h | 数据字典 |

### 9.4 前端优化

- Vite 构建：代码分割 + Tree Shaking
- 路由懒加载：按模块动态 import
- 图片压缩：WebP 格式 + CDN
- Gzip：Nginx 开启 gzip 压缩

---

## 十、目录结构（Docker 相关）

```
saas/
├── docker/
│   ├── nginx/
│   │   ├── conf.d/
│   │   │   └── default.conf
│   │   └── ssl/              # SSL 证书
│   ├── php/
│   │   ├── Dockerfile
│   │   └── php.ini
│   └── mysql/
│       ├── my.cnf
│       └── init/             # 初始化 SQL
├── scripts/
│   ├── backup-db.sh
│   ├── backup-files.sh
│   └── deploy.sh
├── docker-compose.yml
├── .env
└── .env.example
```
