# 部署指南

基于 Docker Compose 的单容器部署方案。容器内 Express 同时托管：

- `/api/*` — API
- `/uploads/*` — 图章静态资源
- 其余路径 — React SPA（`packages/web/dist`）

外部通过宿主机 Nginx 反向代理到容器的 3000 端口（仅绑定 127.0.0.1，不直接暴露）。

---

## 1. 前置条件

- Linux 服务器（Docker ≥ 24、Docker Compose v2）
- 已解析到服务器的域名（例 `stamp.example.com`）
- 已签发的 SSL 证书（Let's Encrypt / acme.sh 等任选）

---

## 2. 拉取代码并配置环境变量

```bash
git clone <repo-url> citywalk-stamp
cd citywalk-stamp

cp .env.production.example .env
vim .env   # 填入 JWT_SECRET / ADMIN_API_KEY / SITE_URL
```

必填项：

| 变量 | 说明 |
|---|---|
| `JWT_SECRET` | 用户 JWT 签名密钥，建议 `openssl rand -hex 32` 生成 |
| `ADMIN_API_KEY` | 管理后台访问密钥，同上 |
| `SITE_URL` | 对外域名，例 `https://stamp.example.com`（影响 QR 码链接） |

---

## 3. 启动容器

```bash
docker compose up -d --build
```

首次启动会自动完成：

- Prisma migrate deploy（建表）
- 把内置的 16 枚图章素材复制到 `./uploads/stamps/`

查看日志确认启动成功：

```bash
docker compose logs -f app
# 看到 "Server running on http://localhost:3000" 即 OK
```

---

## 4. 首次导入图章数据

容器默认不自动 seed 数据库（避免覆盖后续运营数据）。首次部署需手动执行一次：

```bash
docker compose exec app pnpm db:seed
```

> 再次执行会**清空并重建**所有图章，同时级联删除用户的收集记录。生产环境请勿随意重跑。

---

## 5. 配置 Nginx 反向代理

在宿主机上配置 Nginx：

```nginx
server {
    listen 80;
    server_name stamp.example.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate     /etc/letsencrypt/live/stamp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/stamp.example.com/privkey.pem;

    # 单入口反代：容器里 Express 自己处理 /api、/uploads、SPA 路由
    location / {
        proxy_pass http://127.0.0.1:3001;
        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;
    }

    # 静态资源缓存（可选）
    location ^~ /uploads/ {
        proxy_pass http://127.0.0.1:3001;
        proxy_set_header Host $host;
        expires 7d;
        add_header Cache-Control "public, max-age=604800";
    }

    # 上传体积（管理后台上传图章）
    client_max_body_size 10m;
}
```

测试并 reload：

```bash
sudo nginx -t && sudo systemctl reload nginx
```

访问 `https://stamp.example.com` 即可看到首页。

---

## 6. 数据持久化与备份

容器两个 bind mount：

- `./data/` — SQLite 数据库（`prod.db`）
- `./uploads/` — 图章图片（含首次填充的 16 枚 + 后续管理后台上传的）

备份：

```bash
# 停机备份最稳妥（SQLite 文件锁）
docker compose stop app
tar czf backup-$(date +%F).tgz data/ uploads/ .env
docker compose start app
```

---

## 7. 常用运维命令

```bash
# 查看日志
docker compose logs -f app

# 进入容器
docker compose exec app sh

# 重启
docker compose restart app

# 升级（拉新代码后重建）
git pull
docker compose up -d --build

# 查看当前图章数
docker compose exec app sh -c 'echo "SELECT COUNT(*) FROM Stamp;" | sqlite3 /app/data/prod.db' \
  || docker compose exec app node -e "
    import('@prisma/client').then(async ({PrismaClient}) => {
      const p = new PrismaClient();
      console.log(await p.stamp.count());
      await p.\$disconnect();
    });
  "

# 完全重置（会删除所有用户和收集数据，谨慎）
docker compose down
rm -rf data/ uploads/
docker compose up -d --build
docker compose exec app pnpm db:seed
```

---

## 8. 故障排查

- **容器启动失败 `PrismaClientInitializationError`**：检查 `./data/` 目录权限、`DATABASE_URL` 值
- **图章图片 404**：进入容器 `ls /app/packages/server/uploads/stamps` 看是否被 seed 进来；若空，手动执行 `cp /app/stamps-seed/*.jpg /app/packages/server/uploads/stamps/`
- **QR 码扫出来是 `localhost`**：`.env` 里 `SITE_URL` 没改对，修正后 `docker compose restart app`
- **Nginx 502**：确认容器跑着 `docker compose ps`，端口 `ss -tlnp | grep 3000`
