# 外卖SaaS系统 - 部署运维

## 1. 环境要求

### 1.1 开发环境
- **.NET SDK**：10.0 或更高版本
- **IDE**：Visual Studio 2022 / JetBrains Rider / VS Code
- **数据库**：PostgreSQL 16+
- **缓存**：Redis 7.0+
- **消息队列**：RabbitMQ 3.12+
- **Git**：版本控制
- **Docker Desktop**：容器化开发（可选）

### 1.2 生产环境
- **操作系统**：Linux (Ubuntu 22.04 LTS / CentOS 8+)
- **运行时**：.NET Runtime 10.0
- **Web服务器**：Nginx 1.24+
- **数据库**：PostgreSQL 16+ (主从复制)
- **缓存**：Redis 7.0+ (哨兵模式)
- **消息队列**：RabbitMQ 3.12+ (集群模式)
- **对象存储**：MinIO / 阿里云OSS / 腾讯云COS
- **监控**：Prometheus + Grafana
- **日志**：ELK Stack (Elasticsearch + Logstash + Kibana)

### 1.3 硬件要求（生产环境）
- **应用服务器**：4核8GB内存（最低），推荐8核16GB
- **数据库服务器**：8核16GB内存，SSD存储
- **Redis服务器**：4核8GB内存
- **负载均衡器**：2核4GB内存

## 2. 本地开发环境搭建

### 2.1 安装.NET SDK
```bash
# Windows
# 从官网下载安装：https://dotnet.microsoft.com/download

# Linux (Ubuntu)
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get update
sudo apt-get install -y dotnet-sdk-10.0

# 验证安装
dotnet --version
```

### 2.2 安装PostgreSQL
```bash
# Ubuntu
sudo apt-get update
sudo apt-get install -y postgresql-16 postgresql-contrib-16

# 启动服务
sudo systemctl start postgresql
sudo systemctl enable postgresql

# 创建数据库
sudo -u postgres psql
CREATE DATABASE takeout_saas;
CREATE USER takeout_user WITH PASSWORD 'your_password';
GRANT ALL PRIVILEGES ON DATABASE takeout_saas TO takeout_user;
\q
```

### 2.3 安装Redis
```bash
# Ubuntu
sudo apt-get install -y redis-server

# 启动服务
sudo systemctl start redis-server
sudo systemctl enable redis-server

# 测试连接
redis-cli ping
```

### 2.4 安装RabbitMQ
```bash
# Ubuntu
sudo apt-get install -y rabbitmq-server

# 启动服务
sudo systemctl start rabbitmq-server
sudo systemctl enable rabbitmq-server

# 启用管理插件
sudo rabbitmq-plugins enable rabbitmq_management

# 创建用户
sudo rabbitmqctl add_user admin password
sudo rabbitmqctl set_user_tags admin administrator
sudo rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"

# 访问管理界面：http://localhost:15672
```

### 2.5 使用Docker Compose（推荐）
```yaml
# docker-compose.yml
version: '3.8'

services:
  postgres:
    image: postgres:16
    container_name: takeout_postgres
    environment:
      POSTGRES_DB: takeout_saas
      POSTGRES_USER: takeout_user
      POSTGRES_PASSWORD: your_password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    container_name: takeout_redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

  rabbitmq:
    image: rabbitmq:3.12-management
    container_name: takeout_rabbitmq
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: password
    ports:
      - "5672:5672"
      - "15672:15672"
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq

  minio:
    image: minio/minio:latest
    container_name: takeout_minio
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: admin
      MINIO_ROOT_PASSWORD: password123
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - minio_data:/data

volumes:
  postgres_data:
  redis_data:
  rabbitmq_data:
  minio_data:
```

```bash
# 启动所有服务
docker-compose up -d

# 查看服务状态
docker-compose ps

# 停止服务
docker-compose down
```

### 2.6 配置项目
```bash
# 克隆项目
git clone https://github.com/your-org/takeout-saas.git
cd takeout-saas

# 还原依赖
dotnet restore

# 配置appsettings.Development.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=takeout_saas;Username=takeout_user;Password=your_password"
  },
  "Redis": {
    "Configuration": "localhost:6379"
  },
  "RabbitMQ": {
    "Host": "localhost",
    "Port": 5672,
    "Username": "admin",
    "Password": "password"
  }
}

# 执行数据库迁移
cd src/TakeoutSaaS.Api
dotnet ef database update

# 运行项目
dotnet run
```

### 2.7 EF Core 迁移基线

现已内置 `dotnet-ef` 本地工具与设计时 DbContext 工厂，可直接在命令行生成/更新数据库。运行前可通过环境变量 `TAKEOUTSAAS_APP_CONNECTION`、`TAKEOUTSAAS_IDENTITY_CONNECTION` 覆盖默认连接串（默认指向本地 PostgreSQL）。

```powershell
# 业务主库（TakeoutAppDbContext，含租户/商户/门店/商品/订单等）
dotnet tool run dotnet-ef database update `
  --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
  --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
  --context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext

# 身份库（IdentityDbContext）
dotnet tool run dotnet-ef database update `
  --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
  --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
  --context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext

# 业务/字典库（DictionaryDbContext，归属 AppDatabase）
dotnet tool run dotnet-ef database update `
  --project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
  --startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
  --context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext
```

> Hangfire 使用 Scheduler.ConnectionString 指向的数据库，首次启动服务会自动建表；只需提前创建空数据库并授予账号权限。

## 3. Docker部署

### 3.1 创建Dockerfile
```dockerfile
# Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["src/TakeoutSaaS.Api/TakeoutSaaS.Api.csproj", "src/TakeoutSaaS.Api/"]
COPY ["src/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj", "src/TakeoutSaaS.Application/"]
COPY ["src/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj", "src/TakeoutSaaS.Domain/"]
COPY ["src/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj", "src/TakeoutSaaS.Infrastructure/"]
COPY ["src/TakeoutSaaS.Shared/TakeoutSaaS.Shared.csproj", "src/TakeoutSaaS.Shared/"]
RUN dotnet restore "src/TakeoutSaaS.Api/TakeoutSaaS.Api.csproj"
COPY . .
WORKDIR "/src/src/TakeoutSaaS.Api"
RUN dotnet build "TakeoutSaaS.Api.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "TakeoutSaaS.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "TakeoutSaaS.Api.dll"]
```

### 3.2 构建镜像
```bash
# 构建镜像
docker build -t takeout-saas-api:latest .

# 查看镜像
docker images | grep takeout-saas

# 运行容器
docker run -d \
  --name takeout-api \
  -p 8080:80 \
  -e ASPNETCORE_ENVIRONMENT=Production \
  -e ConnectionStrings__DefaultConnection="Host=postgres;Port=5432;Database=takeout_saas;Username=takeout_user;Password=your_password" \
  takeout-saas-api:latest
```

### 3.3 生产环境Docker Compose
```yaml
# docker-compose.prod.yml
version: '3.8'

services:
  api:
    image: takeout-saas-api:latest
    container_name: takeout_api
    restart: always
    environment:
      ASPNETCORE_ENVIRONMENT: Production
      ConnectionStrings__DefaultConnection: "Host=postgres;Port=5432;Database=takeout_saas;Username=takeout_user;Password=${DB_PASSWORD}"
      Redis__Configuration: "redis:6379"
      RabbitMQ__Host: "rabbitmq"
    ports:
      - "8080:80"
    depends_on:
      - postgres
      - redis
      - rabbitmq
    networks:
      - takeout_network

  nginx:
    image: nginx:latest
    container_name: takeout_nginx
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/ssl:/etc/nginx/ssl
    depends_on:
      - api
    networks:
      - takeout_network

networks:
  takeout_network:
    driver: bridge
```

## 4. Nginx配置

### 4.1 基础配置
```nginx
# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # Gzip压缩
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml text/javascript
               application/json application/javascript application/xml+rss
               application/rss+xml font/truetype font/opentype
               application/vnd.ms-fontobject image/svg+xml;

    # 限流配置
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    include /etc/nginx/conf.d/*.conf;
}
```

### 4.2 API服务配置
```nginx
# /etc/nginx/conf.d/api.conf
upstream api_backend {
    least_conn;
    server api1:80 weight=1 max_fails=3 fail_timeout=30s;
    server api2:80 weight=1 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

server {
    listen 80;
    server_name api.example.com;

    # 重定向到HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # SSL证书配置
    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # 安全头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # 客户端请求体大小限制
    client_max_body_size 10M;

    # API接口
    location /api/ {
        # 限流
        limit_req zone=api_limit burst=20 nodelay;
        limit_conn conn_limit 10;

        proxy_pass http://api_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 超时设置
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # 缓冲设置
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
        proxy_busy_buffers_size 8k;
    }

    # WebSocket
    location /ws {
        proxy_pass http://api_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # WebSocket超时
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }

    # 健康检查
    location /health {
        proxy_pass http://api_backend;
        access_log off;
    }

    # 静态文件缓存
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        proxy_pass http://api_backend;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}
```

## 5. 数据库部署

### 5.1 PostgreSQL主从复制
```bash
# 主库配置 (postgresql.conf)
listen_addresses = '*'
wal_level = replica
max_wal_senders = 10
wal_keep_size = 64MB
hot_standby = on

# 主库配置 (pg_hba.conf)
host replication replicator 192.168.1.0/24 md5

# 创建复制用户
CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'repl_password';

# 从库配置
# 1. 停止从库
sudo systemctl stop postgresql

# 2. 清空从库数据目录
rm -rf /var/lib/postgresql/16/main/*

# 3. 从主库复制数据
pg_basebackup -h master_ip -D /var/lib/postgresql/16/main -U replicator -P -v -R -X stream -C -S replica1

# 4. 启动从库
sudo systemctl start postgresql

# 5. 验证复制状态
# 主库执行
SELECT * FROM pg_stat_replication;
```

### 5.2 数据库备份脚本
```bash
#!/bin/bash
# backup_db.sh

BACKUP_DIR="/backup/postgres"
DATE=$(date +%Y%m%d_%H%M%S)
DB_NAME="takeout_saas"
DB_USER="takeout_user"
RETENTION_DAYS=30

# 创建备份目录
mkdir -p $BACKUP_DIR

# 全量备份
pg_dump -h localhost -U $DB_USER -d $DB_NAME -F c -f $BACKUP_DIR/full_$DATE.dump

# 压缩备份
gzip $BACKUP_DIR/full_$DATE.dump

# 删除过期备份
find $BACKUP_DIR -name "full_*.dump.gz" -mtime +$RETENTION_DAYS -delete

# 上传到对象存储（可选）
# aws s3 cp $BACKUP_DIR/full_$DATE.dump.gz s3://your-bucket/backups/

echo "Backup completed: full_$DATE.dump.gz"
```

### 5.3 定时备份（Crontab）
```bash
# 编辑crontab
crontab -e

# 每天凌晨2点执行备份
0 2 * * * /path/to/backup_db.sh >> /var/log/backup.log 2>&1
```

## TODO：基础设施部署脚本

- [ ] PostgreSQL 主从：整理主库/从库初始化脚本、basebackup 步骤与故障切换手册。
- [ ] Redis 哨兵/集群：补充 redis.conf/sentinel.conf 模板以及一主两从搭建命令。
- [ ] RabbitMQ：编写单节点到镜像队列的安装脚本，记录 VHost、用户、权限、监控等操作。
- [ ] 腾讯云 COS：整理桶创建、ACL、CDN 绑定与密钥轮换流程，并提供 coscmd/SDK 示例。
- [ ] Hangfire 存储：确认 PostgreSQL Schema 初始化脚本，补充定期备份、清理、监控的 SOP。

## 6. Redis部署

### 6.1 Redis哨兵模式
```bash
# redis.conf (主节点)
bind 0.0.0.0
port 6379
requirepass your_password
masterauth your_password

# sentinel.conf
port 26379
sentinel monitor mymaster 192.168.1.100 6379 2
sentinel auth-pass mymaster your_password
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 10000
```

### 6.2 Redis持久化配置
```bash
# redis.conf
# RDB持久化
save 900 1
save 300 10
save 60 10000
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb

# AOF持久化
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
```

## 7. CI/CD配置

### 7.1 GitHub Actions
```yaml
# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: '10.0.x'

    - name: Restore dependencies
      run: dotnet restore

    - name: Build
      run: dotnet build --configuration Release --no-restore

    - name: Test
      run: dotnet test --no-build --verbosity normal

    - name: Publish
      run: dotnet publish src/TakeoutSaaS.Api/TakeoutSaaS.Api.csproj -c Release -o ./publish

    - name: Build Docker image
      run: |
        docker build -t takeout-saas-api:${{ github.sha }} .
        docker tag takeout-saas-api:${{ github.sha }} takeout-saas-api:latest

    - name: Push to Registry
      run: |
        echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
        docker push takeout-saas-api:${{ github.sha }}
        docker push takeout-saas-api:latest

    - name: Deploy to Server
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.SERVER_HOST }}
        username: ${{ secrets.SERVER_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /opt/takeout-saas
          docker-compose pull
          docker-compose up -d
          docker system prune -f
```

### 7.2 GitLab CI
```yaml
# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy

variables:
  DOCKER_IMAGE: registry.example.com/takeout-saas-api

build:
  stage: build
  image: mcr.microsoft.com/dotnet/sdk:10.0
  script:
    - dotnet restore
    - dotnet build --configuration Release
  artifacts:
    paths:
      - src/*/bin/Release/

test:
  stage: test
  image: mcr.microsoft.com/dotnet/sdk:10.0
  script:
    - dotnet test --configuration Release

deploy:
  stage: deploy
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA .
    - docker tag $DOCKER_IMAGE:$CI_COMMIT_SHA $DOCKER_IMAGE:latest
    - docker push $DOCKER_IMAGE:$CI_COMMIT_SHA
    - docker push $DOCKER_IMAGE:latest
  only:
    - main
```

## 8. 监控告警

### 8.1 Prometheus配置
```yaml
# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'takeout-api'
    static_configs:
      - targets: ['api:80']
    metrics_path: '/metrics'

  - job_name: 'postgres'
    static_configs:
      - targets: ['postgres-exporter:9187']

  - job_name: 'redis'
    static_configs:
      - targets: ['redis-exporter:9121']

  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']
```

### 8.2 应用监控指标（OpenTelemetry + Prometheus Exporter）
```csharp
// Program.cs - 指标与探针
builder.Services.AddHealthChecks();
builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics =>
    {
        metrics
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddRuntimeInstrumentation()
            .AddPrometheusExporter(); // /metrics
    });

var app = builder.Build();
app.MapHealthChecks("/healthz");          // 存活/就绪探针
app.MapPrometheusScrapingEndpoint();      // 默认 /metrics
```

自定义业务指标（使用 `System.Diagnostics.Metrics`，由 Prometheus Exporter 暴露）：
```csharp
internal static class BusinessMetrics
{
    private static readonly Meter Meter = new("TakeoutSaaS.App", "1.0.0");
    public static readonly Counter<long> OrdersCreated = Meter.CreateCounter<long>("orders_created_total", "个", "订单创建计数");
    public static readonly Histogram<double> OrderProcessingSeconds = Meter.CreateHistogram<double>("order_processing_duration_seconds", "s", "订单处理耗时");
}
```

Prometheus 抓取示例：见 `deploy/prometheus/prometheus.yml`，默认拉取 `/metrics`，告警规则见 `deploy/prometheus/alert.rules.yml`。

### 8.3 Grafana仪表板
```json
{
  "dashboard": {
    "title": "外卖SaaS系统监控",
    "panels": [
      {
        "title": "API请求速率",
        "targets": [
          {
            "expr": "rate(http_requests_total[5m])"
          }
        ]
      },
      {
        "title": "订单创建数",
        "targets": [
          {
            "expr": "increase(orders_created_total[1h])"
          }
        ]
      },
      {
        "title": "数据库连接数",
        "targets": [
          {
            "expr": "pg_stat_activity_count"
          }
        ]
      }
    ]
  }
}
```

### 8.4 告警规则
```yaml
# alert.rules.yml
groups:
  - name: takeout_alerts
    interval: 30s
    rules:
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "高错误率告警"
          description: "API错误率超过5%"

      - alert: DatabaseDown
        expr: up{job="postgres"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "数据库宕机"
          description: "PostgreSQL数据库不可用"

      - alert: HighMemoryUsage
        expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes > 0.9
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "内存使用率过高"
          description: "内存使用率超过90%"
```

## 9. 日志管理

### 9.1 Serilog配置
```json
{
  "Serilog": {
    "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Elasticsearch"],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/log-.txt",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30
        }
      },
      {
        "Name": "Elasticsearch",
        "Args": {
          "nodeUris": "http://elasticsearch:9200",
          "indexFormat": "takeout-logs-{0:yyyy.MM.dd}",
          "autoRegisterTemplate": true
        }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
  }
}
```

### 9.2 ELK Stack部署
```yaml
# docker-compose.elk.yml
version: '3.8'

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    environment:
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - "9200:9200"
    volumes:
      - es_data:/usr/share/elasticsearch/data

  logstash:
    image: docker.elastic.co/logstash/logstash:8.11.0
    volumes:
      - ./logstash/pipeline:/usr/share/logstash/pipeline
    ports:
      - "5044:5044"
    depends_on:
      - elasticsearch

  kibana:
    image: docker.elastic.co/kibana/kibana:8.11.0
    ports:
      - "5601:5601"
    environment:
      ELASTICSEARCH_HOSTS: http://elasticsearch:9200
    depends_on:
      - elasticsearch

volumes:
  es_data:
```

## 10. 安全加固

### 10.1 防火墙配置
```bash
# UFW防火墙
sudo ufw enable
sudo ufw allow 22/tcp    # SSH
sudo ufw allow 80/tcp    # HTTP
sudo ufw allow 443/tcp   # HTTPS
sudo ufw deny 5432/tcp   # 禁止外部访问数据库
sudo ufw deny 6379/tcp   # 禁止外部访问Redis
```

### 10.2 SSL证书（Let's Encrypt）
```bash
# 安装Certbot
sudo apt-get install certbot python3-certbot-nginx

# 获取证书
sudo certbot --nginx -d api.example.com

# 自动续期
sudo certbot renew --dry-run

# 添加定时任务
0 3 * * * certbot renew --quiet
```

### 10.3 应用安全配置
```csharp
// Program.cs
builder.Services.AddHsts(options =>
{
    options.MaxAge = TimeSpan.FromDays(365);
    options.IncludeSubDomains = true;
    options.Preload = true;
});

builder.Services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
    options.HttpsPort = 443;
});

// 添加安全头
app.Use(async (context, next) =>
{
    context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
    context.Response.Headers.Add("X-Frame-Options", "DENY");
    context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
    context.Response.Headers.Add("Referrer-Policy", "no-referrer");
    await next();
});
```

## 11. 性能优化

### 11.1 数据库连接池
```json
{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=takeout_saas;Username=user;Password=pass;Pooling=true;MinPoolSize=5;MaxPoolSize=100;ConnectionLifetime=300"
  }
}
```

### 11.2 Redis连接池
```csharp
services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = configuration["Redis:Configuration"];
    options.InstanceName = "TakeoutSaaS:";
});
```

### 11.3 响应压缩
```csharp
builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<GzipCompressionProvider>();
    options.Providers.Add<BrotliCompressionProvider>();
});
```

## 12. 故障恢复

### 12.1 数据库恢复
```bash
# 从备份恢复
pg_restore -h localhost -U takeout_user -d takeout_saas -v /backup/full_20240101.dump

# PITR恢复到指定时间点
# 1. 停止数据库
sudo systemctl stop postgresql

# 2. 恢复基础备份
rm -rf /var/lib/postgresql/16/main/*
tar -xzf /backup/base_backup.tar.gz -C /var/lib/postgresql/16/main/

# 3. 配置recovery.conf
restore_command = 'cp /backup/wal_archive/%f %p'
recovery_target_time = '2024-01-01 12:00:00'

# 4. 启动数据库
sudo systemctl start postgresql
```

### 12.2 应用回滚
```bash
# Docker回滚到上一个版本
docker-compose down
docker-compose up -d --force-recreate --no-deps api

# 或使用特定版本
docker pull takeout-saas-api:previous-version
docker-compose up -d
```

## 13. 网关 TakeoutSaaS.ApiGateway 部署

1. **部署拓扑**  
   - Nginx 负责域名 `kjkj.qiyuesns.cn`（含后续 HTTPS 证书），并将所有流量反代到本机 `http://127.0.0.1:5000`。  
   - .NET 网关容器（TakeoutSaaS.ApiGateway）负责 YARP 路由、限流、日志与 OpenTelemetry 埋点，向下游 49.7.179.246 的 Admin/User/Mini API 转发。

2. **构建与运行**  
   ```bash
   # 构建镜像
   docker build -f src/Gateway/TakeoutSaaS.ApiGateway/Dockerfile -t takeoutsaas/apigateway:latest .

   # 启动容器（生产环境建议挂载独立配置）
   docker run -d --name takeout-gateway -p 5000:5000 ^
     -e ASPNETCORE_ENVIRONMENT=Production ^
     -v /opt/takeoutsaas/gateway/appsettings.Production.json:/app/appsettings.Production.json ^
     takeoutsaas/apigateway:latest
   ```
   - `appsettings.json` 默认将 `/api/admin|mini|user/**` 指向主应用服务器（7801/7701/7901 端口）。  
   - `appsettings.Development.json` 可在本地覆盖为 `localhost:5001/5002/5003`，无需改代码。

3. **Nginx 参考配置**
   ```nginx
   server {
       listen 80;
       server_name kjkj.qiyuesns.cn;

       location / {
           proxy_pass http://127.0.0.1:5000;
           proxy_set_header Host $host;
           proxy_set_header X-Real-IP $remote_addr;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           proxy_set_header X-Forwarded-Proto $scheme;
       }
   }
   ```
   - 后续启用 HTTPS 时，将 `listen 443 ssl` 与证书配置加入同一 `server`，其余保持不变。

4. **关键配置项说明**
   - `Gateway:RateLimiting`：按客户端 IP 固定窗口限流（默认 300 次/60 秒），可通过配置文件调整或关闭。  
   - `ReverseProxy`：集中声明路由规则（`/api/{service}/**`），后端地址变更时只需改配置即可。  
   - `OpenTelemetry`：默认开启 OTLP 导出，Collector 地址通过 `OpenTelemetry:OtlpEndpoint` 指定。  
   - `Serilog`：统一输出到控制台，日志采集器可以直接收集 Docker stdout。

5. **健康检查**  
   - `GET /healthz`：基础健康，用于探活或监控告警。  
   - `GET /`：返回服务元信息，可作为简易诊断接口。
