feat: 阿里云 OSS 迁移至腾讯云 COS + 完善部署配置
COS 迁移: - 后端签名服务改为 COS HMAC-SHA1 表单直传签名 - config.py: OSS_* 配置项替换为 COS_SECRET_ID/KEY/REGION/BUCKET_NAME/CDN_DOMAIN - upload.py: UploadPolicyResponse 改为 COS 字段 - 前端 useOSSUpload hook: FormData 字段改为 COS 格式 - 前端 api.ts: UploadPolicyResponse 类型对齐 部署配置: - docker-compose.yml: 新增 Nginx + 前端容器,数据卷宿主机持久化 - Nginx: HTTPS + HTTP/2 + SSE 长连接 + API/前端反向代理 - backup.sh: PostgreSQL 每日备份 → 本地 + COS - .env.example: 更新为 COS 配置模板 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f02b3f4098
commit
8ab2d869fc
@ -106,8 +106,9 @@ useEffect(() => { loadData() }, [loadData])
|
||||
- 配置项:`AI_PROVIDER`, `AI_API_KEY`, `AI_API_BASE_URL`
|
||||
|
||||
### 文件上传
|
||||
- 阿里云 OSS 直传,前端通过 `useOSSUpload` hook 处理
|
||||
- 流程:`api.getUploadPolicy()` → POST 到 OSS → `api.fileUploaded()` 回调
|
||||
- 腾讯云 COS 直传,前端通过 `useOSSUpload` hook 处理
|
||||
- 流程:`api.getUploadPolicy()` → POST 到 COS → `api.fileUploaded()` 回调
|
||||
- COS 签名:HMAC-SHA1,字段包括 `q-sign-algorithm`、`q-ak`、`q-key-time`、`q-signature`、`policy`
|
||||
|
||||
### 实时推送
|
||||
- SSE (Server-Sent Events),端点 `/api/v1/sse/events`
|
||||
|
||||
@ -8,12 +8,16 @@
|
||||
APP_NAME=秒思智能审核平台
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=false
|
||||
ENVIRONMENT=production
|
||||
|
||||
# --- 数据库 ---
|
||||
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/miaosi
|
||||
POSTGRES_USER=miaosi
|
||||
POSTGRES_PASSWORD=change-me-in-production
|
||||
POSTGRES_DB=miaosi
|
||||
DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
||||
|
||||
# --- Redis ---
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
|
||||
# --- JWT ---
|
||||
# 生产环境务必更换为随机密钥: python -c "import secrets; print(secrets.token_urlsafe(64))"
|
||||
@ -26,12 +30,20 @@ AI_PROVIDER=oneapi
|
||||
AI_API_KEY=
|
||||
AI_API_BASE_URL=
|
||||
|
||||
# --- 阿里云 OSS ---
|
||||
OSS_ACCESS_KEY_ID=
|
||||
OSS_ACCESS_KEY_SECRET=
|
||||
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
|
||||
OSS_BUCKET_NAME=miaosi-files
|
||||
OSS_BUCKET_DOMAIN=
|
||||
# --- 腾讯云 COS ---
|
||||
COS_SECRET_ID=
|
||||
COS_SECRET_KEY=
|
||||
COS_REGION=ap-guangzhou
|
||||
COS_BUCKET_NAME=miaosi-files-1250000000
|
||||
COS_CDN_DOMAIN=
|
||||
|
||||
# --- 邮件 SMTP ---
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=465
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM_NAME=秒思智能审核平台
|
||||
SMTP_USE_SSL=true
|
||||
|
||||
# --- 加密密钥 ---
|
||||
# 用于加密存储 API 密钥等敏感数据
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
"""
|
||||
文件上传 API
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from app.services.oss import generate_upload_policy, get_file_url
|
||||
from app.config import settings
|
||||
from app.models.user import User
|
||||
from app.api.deps import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/upload", tags=["文件上传"])
|
||||
|
||||
@ -19,10 +21,12 @@ class UploadPolicyRequest(BaseModel):
|
||||
|
||||
|
||||
class UploadPolicyResponse(BaseModel):
|
||||
"""上传凭证响应"""
|
||||
access_key_id: str
|
||||
"""COS 直传凭证响应"""
|
||||
q_sign_algorithm: str
|
||||
q_ak: str
|
||||
q_key_time: str
|
||||
q_signature: str
|
||||
policy: str
|
||||
signature: str
|
||||
host: str
|
||||
dir: str
|
||||
expire: int
|
||||
@ -49,11 +53,12 @@ class FileUploadedResponse(BaseModel):
|
||||
@router.post("/policy", response_model=UploadPolicyResponse)
|
||||
async def get_upload_policy(
|
||||
request: UploadPolicyRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
获取 OSS 直传凭证
|
||||
获取 COS 直传凭证
|
||||
|
||||
前端使用此凭证直接上传文件到阿里云 OSS,无需经过后端。
|
||||
前端使用此凭证直接上传文件到腾讯云 COS,无需经过后端。
|
||||
|
||||
文件类型说明:
|
||||
- script: 脚本文档 (docx, pdf, xlsx, txt, pptx)
|
||||
@ -87,9 +92,11 @@ async def get_upload_policy(
|
||||
)
|
||||
|
||||
return UploadPolicyResponse(
|
||||
access_key_id=policy["accessKeyId"],
|
||||
q_sign_algorithm=policy["q_sign_algorithm"],
|
||||
q_ak=policy["q_ak"],
|
||||
q_key_time=policy["q_key_time"],
|
||||
q_signature=policy["q_signature"],
|
||||
policy=policy["policy"],
|
||||
signature=policy["signature"],
|
||||
host=policy["host"],
|
||||
dir=policy["dir"],
|
||||
expire=policy["expire"],
|
||||
@ -100,6 +107,7 @@ async def get_upload_policy(
|
||||
@router.post("/complete", response_model=FileUploadedResponse)
|
||||
async def file_uploaded(
|
||||
request: FileUploadedRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
文件上传完成回调
|
||||
|
||||
@ -32,12 +32,12 @@ class Settings(BaseSettings):
|
||||
AI_API_KEY: str = "" # 中转服务商的 API Key
|
||||
AI_API_BASE_URL: str = "" # 中转服务商的 Base URL,如 https://api.oneinall.ai/v1
|
||||
|
||||
# 阿里云 OSS 配置
|
||||
OSS_ACCESS_KEY_ID: str = ""
|
||||
OSS_ACCESS_KEY_SECRET: str = ""
|
||||
OSS_ENDPOINT: str = "oss-cn-hangzhou.aliyuncs.com"
|
||||
OSS_BUCKET_NAME: str = "miaosi-files"
|
||||
OSS_BUCKET_DOMAIN: str = "" # 公开访问域名,如 https://miaosi-files.oss-cn-hangzhou.aliyuncs.com
|
||||
# 腾讯云 COS 配置
|
||||
COS_SECRET_ID: str = ""
|
||||
COS_SECRET_KEY: str = ""
|
||||
COS_REGION: str = "ap-guangzhou"
|
||||
COS_BUCKET_NAME: str = "miaosi-files-1250000000"
|
||||
COS_CDN_DOMAIN: str = "" # CDN 自定义域名,空则用 COS 源站
|
||||
|
||||
# 邮件 SMTP
|
||||
SMTP_HOST: str = ""
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""
|
||||
阿里云 OSS 服务
|
||||
腾讯云 COS 服务 — 表单直传签名
|
||||
"""
|
||||
import time
|
||||
import hmac
|
||||
@ -17,99 +17,109 @@ def generate_upload_policy(
|
||||
upload_dir: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
生成前端直传 OSS 所需的 Policy 和签名
|
||||
生成前端直传 COS 所需的 Policy 和签名
|
||||
|
||||
COS 表单直传签名流程:
|
||||
1. key_time = "{start_time};{end_time}"
|
||||
2. sign_key = HMAC-SHA1(secret_key, key_time)
|
||||
3. policy JSON → Base64 编码
|
||||
4. string_to_sign = SHA1(policy_base64)
|
||||
5. signature = HMAC-SHA1(sign_key, string_to_sign)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"accessKeyId": "...",
|
||||
"q_sign_algorithm": "sha1",
|
||||
"q_ak": "SecretId",
|
||||
"q_key_time": "{start};{end}",
|
||||
"q_signature": "...",
|
||||
"policy": "base64 encoded policy",
|
||||
"signature": "...",
|
||||
"host": "https://bucket.oss-cn-hangzhou.aliyuncs.com",
|
||||
"host": "https://bucket.cos.region.myqcloud.com",
|
||||
"dir": "uploads/2026/02/",
|
||||
"expire": 1234567890
|
||||
"expire": 1234567890,
|
||||
}
|
||||
"""
|
||||
if not settings.OSS_ACCESS_KEY_ID or not settings.OSS_ACCESS_KEY_SECRET:
|
||||
raise ValueError("OSS 配置未设置")
|
||||
if not settings.COS_SECRET_ID or not settings.COS_SECRET_KEY:
|
||||
raise ValueError("COS 配置未设置")
|
||||
|
||||
# 计算过期时间
|
||||
expire_time = int(time.time()) + expire_seconds
|
||||
expire_date = datetime.utcfromtimestamp(expire_time).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
# 计算时间范围
|
||||
start_time = int(time.time())
|
||||
end_time = start_time + expire_seconds
|
||||
|
||||
# key_time: "{start};{end}"
|
||||
key_time = f"{start_time};{end_time}"
|
||||
|
||||
# 默认上传目录:uploads/年/月/
|
||||
if upload_dir is None:
|
||||
now = datetime.now()
|
||||
upload_dir = f"uploads/{now.year}/{now.month:02d}/"
|
||||
|
||||
# 构建 Policy
|
||||
# 1. sign_key = HMAC-SHA1(secret_key, key_time)
|
||||
sign_key = hmac.new(
|
||||
settings.COS_SECRET_KEY.encode(),
|
||||
key_time.encode(),
|
||||
hashlib.sha1,
|
||||
).hexdigest()
|
||||
|
||||
# 2. 构建 Policy(COS 表单上传 Policy 格式)
|
||||
policy_dict = {
|
||||
"expiration": expire_date,
|
||||
"expiration": datetime.utcfromtimestamp(end_time).strftime(
|
||||
"%Y-%m-%dT%H:%M:%S.000Z"
|
||||
),
|
||||
"conditions": [
|
||||
{"bucket": settings.OSS_BUCKET_NAME},
|
||||
{"bucket": settings.COS_BUCKET_NAME},
|
||||
["starts-with", "$key", upload_dir],
|
||||
{"q-sign-algorithm": "sha1"},
|
||||
{"q-ak": settings.COS_SECRET_ID},
|
||||
{"q-sign-time": key_time},
|
||||
["content-length-range", 0, max_size_mb * 1024 * 1024],
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
# Base64 编码 Policy
|
||||
# 3. Base64 编码 Policy
|
||||
policy_json = json.dumps(policy_dict)
|
||||
policy_base64 = base64.b64encode(policy_json.encode()).decode()
|
||||
|
||||
# 计算签名
|
||||
signature = base64.b64encode(
|
||||
hmac.new(
|
||||
settings.OSS_ACCESS_KEY_SECRET.encode(),
|
||||
policy_base64.encode(),
|
||||
hashlib.sha1
|
||||
).digest()
|
||||
).decode()
|
||||
# 4. string_to_sign = SHA1(policy_base64)
|
||||
string_to_sign = hashlib.sha1(policy_base64.encode()).hexdigest()
|
||||
|
||||
# 5. signature = HMAC-SHA1(sign_key, string_to_sign)
|
||||
signature = hmac.new(
|
||||
sign_key.encode(),
|
||||
string_to_sign.encode(),
|
||||
hashlib.sha1,
|
||||
).hexdigest()
|
||||
|
||||
# 构建 Host
|
||||
host = settings.OSS_BUCKET_DOMAIN
|
||||
if not host:
|
||||
host = f"https://{settings.OSS_BUCKET_NAME}.{settings.OSS_ENDPOINT}"
|
||||
host = f"https://{settings.COS_BUCKET_NAME}.cos.{settings.COS_REGION}.myqcloud.com"
|
||||
|
||||
return {
|
||||
"accessKeyId": settings.OSS_ACCESS_KEY_ID,
|
||||
"q_sign_algorithm": "sha1",
|
||||
"q_ak": settings.COS_SECRET_ID,
|
||||
"q_key_time": key_time,
|
||||
"q_signature": signature,
|
||||
"policy": policy_base64,
|
||||
"signature": signature,
|
||||
"host": host,
|
||||
"dir": upload_dir,
|
||||
"expire": expire_time,
|
||||
"expire": end_time,
|
||||
}
|
||||
|
||||
|
||||
def generate_sts_token(
|
||||
role_arn: str,
|
||||
session_name: str = "miaosi-upload",
|
||||
duration_seconds: int = 3600,
|
||||
) -> dict:
|
||||
"""
|
||||
生成 STS 临时凭证(需要配置 RAM 角色)
|
||||
|
||||
当前使用 Policy 签名方式,STS 方式为可选增强。
|
||||
如需启用 STS,请安装 aliyun-python-sdk-sts 并配置 RAM 角色。
|
||||
"""
|
||||
# 回退到 Policy 签名方式
|
||||
return generate_upload_policy(
|
||||
max_size_mb=settings.MAX_FILE_SIZE_MB,
|
||||
expire_seconds=duration_seconds,
|
||||
)
|
||||
|
||||
|
||||
def get_file_url(file_key: str) -> str:
|
||||
"""
|
||||
获取文件的公开访问 URL
|
||||
获取文件的访问 URL
|
||||
|
||||
优先使用 CDN 域名,否则用 COS 源站域名。
|
||||
|
||||
Args:
|
||||
file_key: 文件在 OSS 中的 key,如 "uploads/2026/02/video.mp4"
|
||||
file_key: 文件在 COS 中的 key,如 "uploads/2026/02/video.mp4"
|
||||
|
||||
Returns:
|
||||
完整的访问 URL
|
||||
"""
|
||||
host = settings.OSS_BUCKET_DOMAIN
|
||||
if not host:
|
||||
host = f"https://{settings.OSS_BUCKET_NAME}.{settings.OSS_ENDPOINT}"
|
||||
if settings.COS_CDN_DOMAIN:
|
||||
host = settings.COS_CDN_DOMAIN
|
||||
else:
|
||||
host = f"https://{settings.COS_BUCKET_NAME}.cos.{settings.COS_REGION}.myqcloud.com"
|
||||
|
||||
# 确保 host 以 https:// 开头
|
||||
if not host.startswith("http"):
|
||||
@ -129,26 +139,22 @@ def parse_file_key_from_url(url: str) -> str:
|
||||
从完整 URL 解析出文件 key
|
||||
|
||||
Args:
|
||||
url: 完整的 OSS URL
|
||||
url: 完整的 COS URL
|
||||
|
||||
Returns:
|
||||
文件 key
|
||||
"""
|
||||
host = settings.OSS_BUCKET_DOMAIN
|
||||
if not host:
|
||||
host = f"https://{settings.OSS_BUCKET_NAME}.{settings.OSS_ENDPOINT}"
|
||||
# 尝试移除 CDN 域名
|
||||
if settings.COS_CDN_DOMAIN:
|
||||
cdn = settings.COS_CDN_DOMAIN.rstrip("/")
|
||||
if not cdn.startswith("http"):
|
||||
cdn = f"https://{cdn}"
|
||||
if url.startswith(cdn):
|
||||
return url[len(cdn):].lstrip("/")
|
||||
|
||||
# 移除 host 前缀
|
||||
if url.startswith(host):
|
||||
return url[len(host):].lstrip("/")
|
||||
|
||||
# 尝试其他格式
|
||||
if settings.OSS_BUCKET_NAME in url:
|
||||
# 格式: https://bucket.endpoint/key
|
||||
parts = url.split(settings.OSS_BUCKET_NAME + ".")
|
||||
if len(parts) > 1:
|
||||
key_part = parts[1].split("/", 1)
|
||||
if len(key_part) > 1:
|
||||
return key_part[1]
|
||||
# 尝试移除 COS 源站域名
|
||||
cos_host = f"https://{settings.COS_BUCKET_NAME}.cos.{settings.COS_REGION}.myqcloud.com"
|
||||
if url.startswith(cos_host):
|
||||
return url[len(cos_host):].lstrip("/")
|
||||
|
||||
return url
|
||||
|
||||
@ -6,13 +6,11 @@ services:
|
||||
image: postgres:16-alpine
|
||||
container_name: miaosi-postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: miaosi
|
||||
ports:
|
||||
- "5432:5432"
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-miaosi}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
@ -23,10 +21,8 @@ services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: miaosi-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
- ./data/redis:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
@ -39,21 +35,18 @@ services:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: miaosi-api
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/miaosi
|
||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-miaosi}
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
DEBUG: "true"
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./app:/app/app
|
||||
- video_temp:/tmp/videos
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
# Celery Worker
|
||||
celery-worker:
|
||||
@ -62,15 +55,16 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
container_name: miaosi-celery-worker
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/miaosi
|
||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-miaosi}
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./app:/app/app
|
||||
- video_temp:/tmp/videos
|
||||
command: celery -A app.celery_app worker -l info -Q default,review -c 2
|
||||
|
||||
@ -81,15 +75,42 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
container_name: miaosi-celery-beat
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/miaosi
|
||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-miaosi}
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- celery-worker
|
||||
volumes:
|
||||
- ./app:/app/app
|
||||
redis:
|
||||
condition: service_healthy
|
||||
celery-worker: {}
|
||||
command: celery -A app.celery_app beat -l info
|
||||
|
||||
# Next.js 前端
|
||||
frontend:
|
||||
build:
|
||||
context: ../frontend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-https://your-domain.com}
|
||||
NEXT_PUBLIC_USE_MOCK: "false"
|
||||
container_name: miaosi-frontend
|
||||
depends_on:
|
||||
- api
|
||||
|
||||
# Nginx 反向代理
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: miaosi-nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- /etc/letsencrypt:/etc/letsencrypt:ro
|
||||
depends_on:
|
||||
- api
|
||||
- frontend
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
video_temp:
|
||||
|
||||
50
backend/nginx/conf.d/default.conf
Normal file
50
backend/nginx/conf.d/default.conf
Normal file
@ -0,0 +1,50 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-domain.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
# SSE 代理(长连接,必须在 /api/ 之前匹配)
|
||||
location /api/v1/sse/ {
|
||||
proxy_pass http://api:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
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_buffering off;
|
||||
proxy_cache off;
|
||||
chunked_transfer_encoding off;
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
}
|
||||
|
||||
# API 代理
|
||||
location /api/ {
|
||||
proxy_pass http://api:8000;
|
||||
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 / {
|
||||
proxy_pass http://frontend:3000;
|
||||
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;
|
||||
}
|
||||
}
|
||||
43
backend/nginx/nginx.conf
Normal file
43
backend/nginx/nginx.conf
Normal file
@ -0,0 +1,43 @@
|
||||
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;
|
||||
|
||||
# Gzip 压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
gzip_min_length 1000;
|
||||
|
||||
# 安全头
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# 上传大小限制(与后端 MAX_FILE_SIZE_MB 对齐)
|
||||
client_max_body_size 500m;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
49
backend/scripts/backup.sh
Executable file
49
backend/scripts/backup.sh
Executable file
@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# ===========================
|
||||
# PostgreSQL 每日备份脚本
|
||||
# 备份到本地 + 上传到腾讯云 COS
|
||||
# ===========================
|
||||
# 配合 crontab 使用:
|
||||
# 0 3 * * * /path/to/backup.sh >> /var/log/miaosi-backup.log 2>&1
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- 配置 ----
|
||||
BACKUP_DIR="${BACKUP_DIR:-/var/backups/miaosi}"
|
||||
POSTGRES_CONTAINER="${POSTGRES_CONTAINER:-miaosi-postgres}"
|
||||
POSTGRES_USER="${POSTGRES_USER:-postgres}"
|
||||
POSTGRES_DB="${POSTGRES_DB:-miaosi}"
|
||||
RETAIN_DAYS="${RETAIN_DAYS:-7}"
|
||||
|
||||
# COS 备份桶(需要先安装 coscli 并配置好凭证)
|
||||
COS_BACKUP_BUCKET="${COS_BACKUP_BUCKET:-}"
|
||||
COS_BACKUP_PREFIX="${COS_BACKUP_PREFIX:-backups/postgres}"
|
||||
|
||||
# ---- 执行 ----
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
FILENAME="miaosi_${DATE}.sql.gz"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
echo "[$(date)] 开始备份数据库 ${POSTGRES_DB}..."
|
||||
|
||||
# 1. pg_dump 导出并压缩
|
||||
docker exec "$POSTGRES_CONTAINER" pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip > "${BACKUP_DIR}/${FILENAME}"
|
||||
|
||||
echo "[$(date)] 本地备份完成: ${BACKUP_DIR}/${FILENAME}"
|
||||
|
||||
# 2. 上传到 COS(如果配置了备份桶)
|
||||
if [ -n "$COS_BACKUP_BUCKET" ]; then
|
||||
if command -v coscli &> /dev/null; then
|
||||
coscli cp "${BACKUP_DIR}/${FILENAME}" "cos://${COS_BACKUP_BUCKET}/${COS_BACKUP_PREFIX}/${FILENAME}"
|
||||
echo "[$(date)] 已上传到 COS: ${COS_BACKUP_BUCKET}/${COS_BACKUP_PREFIX}/${FILENAME}"
|
||||
else
|
||||
echo "[$(date)] 警告: coscli 未安装,跳过 COS 上传"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. 清理过期的本地备份
|
||||
find "$BACKUP_DIR" -name "miaosi_*.sql.gz" -mtime +"$RETAIN_DAYS" -delete
|
||||
echo "[$(date)] 已清理 ${RETAIN_DAYS} 天前的本地备份"
|
||||
|
||||
echo "[$(date)] 备份完成"
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
# --- API 地址 ---
|
||||
# 后端 API 基础 URL(浏览器端访问)
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_API_BASE_URL=https://your-domain.com
|
||||
|
||||
# --- Mock 模式 ---
|
||||
# 设为 true 使用前端 mock 数据(development 环境下默认开启)
|
||||
|
||||
@ -57,17 +57,19 @@ export function useOSSUpload(fileType: string = 'general'): UseOSSUploadReturn {
|
||||
setProgress(10)
|
||||
const policy = await api.getUploadPolicy(fileType)
|
||||
|
||||
// 2. 构建 OSS 直传 FormData
|
||||
// 2. 构建 COS 直传 FormData
|
||||
const fileKey = `${policy.dir}${Date.now()}_${file.name}`
|
||||
const formData = new FormData()
|
||||
formData.append('key', fileKey)
|
||||
formData.append('q-sign-algorithm', policy.q_sign_algorithm)
|
||||
formData.append('q-ak', policy.q_ak)
|
||||
formData.append('q-key-time', policy.q_key_time)
|
||||
formData.append('q-signature', policy.q_signature)
|
||||
formData.append('policy', policy.policy)
|
||||
formData.append('OSSAccessKeyId', policy.access_key_id)
|
||||
formData.append('signature', policy.signature)
|
||||
formData.append('success_action_status', '200')
|
||||
formData.append('file', file)
|
||||
|
||||
// 3. 上传到 OSS
|
||||
// 3. 上传到 COS
|
||||
setProgress(30)
|
||||
const xhr = new XMLHttpRequest()
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user