修正了部署脚本

codex/dev
mula.liu 2026-04-13 17:27:39 +08:00
parent 60b4c1f3a6
commit 9079d82729
9 changed files with 137 additions and 73 deletions

View File

@ -1,15 +1,28 @@
# ==================== 部署模式说明 ====================
# 1. 默认 Docker 一体化部署(./start.sh / docker-compose.yml
# 使用下方 MYSQL_* / REDIS_* 初始化内置 MySQL、Redis。
# 2. 直接运行后端或外接中间件部署:
# 后端现在也兼容读取当前文件中的 MYSQL_HOST / REDIS_HOST
# 也可以使用 backend/.env 中的 DB_* / REDIS_* 配置。
# ==================== 数据库配置 ==================== # ==================== 数据库配置 ====================
# MySQL数据库配置 # MySQL 初始化参数Docker 内置 MySQL
MYSQL_ROOT_PASSWORD=Unis@123 # 当后端也运行在 Docker 中时,数据库主机应为服务名 `mysql`。
# 只有在宿主机直接运行后端时,才需要改成 127.0.0.1 或实际地址。
MYSQL_HOST=mysql
MYSQL_ROOT_PASSWORD=change_this_password
MYSQL_DATABASE=imeeting MYSQL_DATABASE=imeeting
MYSQL_USER=imeeting MYSQL_USER=imeeting
MYSQL_PASSWORD=Unis@123 MYSQL_PASSWORD=change_this_password
MYSQL_PORT=3306 MYSQL_PORT=3306
# ==================== 缓存配置 ==================== # ==================== 缓存配置 ====================
# Redis配置 # Redis 初始化参数Docker 内置 Redis
# 当后端也运行在 Docker 中时Redis 主机应为服务名 `redis`。
# 只有在宿主机直接运行后端时,才需要改成 127.0.0.1 或实际地址。
REDIS_HOST=redis
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_PASSWORD=Unis@123 REDIS_PASSWORD=change_this_password
REDIS_DB=0 REDIS_DB=0
# ==================== 应用端口配置 ==================== # ==================== 应用端口配置 ====================
@ -17,10 +30,11 @@ REDIS_DB=0
HTTP_PORT=80 HTTP_PORT=80
# ==================== 应用配置 ==================== # ==================== 应用配置 ====================
# 应用访问地址(用于生成外部链接、二维码等) # 应用访问地址(用于生成外部链接、客户端下载链接,以及音频转录时提供给云端拉取音频文件的公网 URL
# - 直接访问: http://服务器IP # - 本地联调可先填写: http://localhost
# - 域名访问: https://your-domain.com 需配置接入服务器Nginx # - 使用云端音频转录时必须改成外部可访问的域名或公网地址不能填写容器名、127.0.0.1 或内网地址
BASE_URL=https://imeeting.unisspace.com # - 不要以 / 结尾,例如: https://your-domain.com
BASE_URL=https://your-domain.com
# 前端API地址通过Nginx代理访问后端 # 前端API地址通过Nginx代理访问后端
VITE_API_BASE_URL=/api VITE_API_BASE_URL=/api

View File

@ -40,7 +40,7 @@
```bash ```bash
# 1. 复制并配置环境变量 # 1. 复制并配置环境变量
cp .env.example .env cp .env.example .env
vim .env # 配置七牛云、LLM密钥 vim .env # 配置 BASE_URL、密码
# 2. 一键启动 # 2. 一键启动
./start.sh ./start.sh
@ -60,9 +60,7 @@ vim .env # 配置七牛云、LLM密钥等
```bash ```bash
# 1. 配置环境变量 # 1. 配置环境变量
cp .env.example .env cp .env.example .env
cp backend/.env.example backend/.env
vim .env # 配置主环境变量 vim .env # 配置主环境变量
vim backend/.env # 配置后端环境变量
# 2. 启动所有服务 # 2. 启动所有服务
docker-compose up -d docker-compose up -d
@ -89,17 +87,12 @@ docker-compose ps
### 必须配置项 ### 必须配置项
编辑 `.env` 文件,修改以下配置: 编辑根目录 `.env` 文件,修改以下配置:
```bash ```bash
# 七牛云存储(必填,否则无法上传文件) # Docker 一体化部署下,后端容器访问内置 MySQL/Redis 使用服务名
QINIU_ACCESS_KEY=your_actual_access_key MYSQL_HOST=mysql
QINIU_SECRET_KEY=your_actual_secret_key REDIS_HOST=redis
QINIU_BUCKET=your_bucket_name
QINIU_DOMAIN=your_domain.clouddn.com
# LLM API必填否则无法使用AI功能
QWEN_API_KEY=your_actual_qwen_api_key
# 生产环境必改密码 # 生产环境必改密码
MYSQL_ROOT_PASSWORD=change_this_password MYSQL_ROOT_PASSWORD=change_this_password
@ -110,7 +103,8 @@ REDIS_PASSWORD=change_this_password
### 可选配置项 ### 可选配置项
```bash ```bash
# 应用访问地址(用于生成二维码等) # 应用访问地址(用于生成外链,以及音频转录时给云端拉取音频文件)
# 如果使用云端音频转录,必须改成外部可访问的域名或公网地址,不能填写 localhost / 127.0.0.1 / 容器名
BASE_URL=http://localhost # 生产环境改为: https://your-domain.com BASE_URL=http://localhost # 生产环境改为: https://your-domain.com
# Nginx端口默认80/443 # Nginx端口默认80/443
@ -122,6 +116,21 @@ HTTPS_PORT=443
# HTTP_PORT=80 # HTTP_PORT=80
``` ```
### 外接 MySQL / Redis 部署
如果使用 `./start-external.sh`,还需要额外配置 `backend/.env`
```bash
cp backend/.env.example backend/.env
vim backend/.env
```
`backend/.env` 主要负责:
- `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_NAME`
- `REDIS_HOST` / `REDIS_PORT` / `REDIS_DB` / `REDIS_PASSWORD`
- 可选的 `BASE_URL`(如显式设置,会覆盖根目录 `.env` 中传给后端容器的值)
### 音频预处理依赖 ### 音频预处理依赖
- Docker 部署:后端容器内已安装 `ffmpeg` - Docker 部署:后端容器内已安装 `ffmpeg`
@ -260,8 +269,9 @@ curl http://localhost/docs
|------|----------| |------|----------|
| 端口被占用 | 修改.env中的HTTP_PORT | | 端口被占用 | 修改.env中的HTTP_PORT |
| 502错误 | 检查backend和frontend是否健康 | | 502错误 | 检查backend和frontend是否健康 |
| 数据库连接失败 | 检查backend/.env配置 | | 数据库连接失败 | 一体化部署检查 `docker-compose.yml` 和根目录 `.env`,外接中间件模式检查 `backend/.env` |
| 前端无法访问API | 检查VITE_API_BASE_URL配置 | | 前端无法访问API | 检查VITE_API_BASE_URL配置 |
| 音频转录拉不到音频文件 | 检查 `BASE_URL` 是否为云端可访问的完整地址 |
| 如何配置HTTPS | 参考本文档中的反向代理章节 | | 如何配置HTTPS | 参考本文档中的反向代理章节 |
详见:`DOCKER_README.md` 故障排查章节 详见:`DOCKER_README.md` 故障排查章节

View File

@ -1,27 +1,30 @@
# ==================== 数据库配置 ==================== # ==================== 数据库配置 ====================
# Docker环境使用容器名称 # 供“直接运行 backend”或“./start-external.sh 外接 MySQL/Redis”使用
DB_HOST=10.100.51.51 # 如果 backend 运行在 Docker 中,且数据库也在同一 Docker 网络,主机名应填服务名(如 mysql
DB_USER=root # 如果是宿主机直接运行 backend再填写 127.0.0.1 或实际地址
DB_PASSWORD=Unis@123 DB_HOST=127.0.0.1
DB_NAME=imeeting_dev DB_USER=imeeting
DB_PASSWORD=change_this_password
DB_NAME=imeeting
DB_PORT=3306 DB_PORT=3306
# ==================== Redis配置 ==================== # ==================== Redis配置 ====================
# Docker环境使用容器名称 # 如果 backend 运行在 Docker 中,且 Redis 也在同一 Docker 网络,主机名应填服务名(如 redis
REDIS_HOST=10.100.51.51 # 如果是宿主机直接运行 backend再填写 127.0.0.1 或实际地址
REDIS_HOST=127.0.0.1
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_DB=0 REDIS_DB=0
REDIS_PASSWORD=Unis@123 REDIS_PASSWORD=change_this_password
# ==================== API配置 ==================== # ==================== API配置 ====================
API_HOST=0.0.0.0 API_HOST=0.0.0.0
API_PORT=8000 API_PORT=8000
# ==================== 应用配置 ==================== # ==================== 应用配置 ====================
# 应用访问地址(用于生成外部链接、二维码等) # 直接运行 backend 时可在这里配置 BASE_URL
# 开发环境: http://localhost # Docker 容器部署时,优先使用仓库根目录 .env 中的 BASE_URL。
# 生产环境: https://your-domain.com # 使用云端音频转录时,必须填写云端可以访问到的公网地址,且不要以 / 结尾。
BASE_URL=http://imeeting.unisspace.com # BASE_URL=https://your-domain.com
# ==================== 转录轮询配置 ==================== # ==================== 转录轮询配置 ====================

View File

@ -2,7 +2,6 @@ from fastapi import APIRouter, Depends, HTTPException
from app.core.database import get_db_connection from app.core.database import get_db_connection
from app.core.auth import get_current_admin_user from app.core.auth import get_current_admin_user
from app.core.response import create_api_response from app.core.response import create_api_response
from app.core.config import QWEN_API_KEY
from app.services.system_config_service import SystemConfigService from app.services.system_config_service import SystemConfigService
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, List from typing import Optional, List
@ -14,6 +13,14 @@ from http import HTTPStatus
router = APIRouter() router = APIRouter()
def _resolve_dashscope_api_key() -> str:
audio_config = SystemConfigService.get_active_audio_model_config("asr") or {}
api_key = str(audio_config.get("api_key") or "").strip()
if not api_key:
raise HTTPException(status_code=500, detail="未配置 DashScope API Key")
return api_key
# ── Request Models ────────────────────────────────────────── # ── Request Models ──────────────────────────────────────────
class CreateGroupRequest(BaseModel): class CreateGroupRequest(BaseModel):
@ -135,7 +142,7 @@ async def delete_group(id: int, current_user: dict = Depends(get_current_admin_u
async def sync_group(id: int, current_user: dict = Depends(get_current_admin_user)): async def sync_group(id: int, current_user: dict = Depends(get_current_admin_user)):
"""同步指定组到阿里云 DashScope""" """同步指定组到阿里云 DashScope"""
try: try:
dashscope.api_key = QWEN_API_KEY dashscope.api_key = _resolve_dashscope_api_key()
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(dictionary=True)

View File

@ -3,12 +3,9 @@ import json
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
# 加载 .env 文件
env_path = Path(__file__).parent.parent.parent / ".env"
load_dotenv(dotenv_path=env_path)
# 基础路径配置 # 基础路径配置
BASE_DIR = Path(__file__).parent.parent.parent BASE_DIR = Path(__file__).parent.parent.parent
REPO_DIR = BASE_DIR.parent
UPLOAD_DIR = BASE_DIR / "uploads" UPLOAD_DIR = BASE_DIR / "uploads"
AUDIO_DIR = UPLOAD_DIR / "audio" AUDIO_DIR = UPLOAD_DIR / "audio"
TEMP_UPLOAD_DIR = UPLOAD_DIR / "temp_audio" TEMP_UPLOAD_DIR = UPLOAD_DIR / "temp_audio"
@ -22,6 +19,42 @@ VOICEPRINT_DIR = USER_DIR
AVATAR_DIR = USER_DIR AVATAR_DIR = USER_DIR
def _load_env_file(dotenv_path: Path) -> None:
if dotenv_path.exists():
load_dotenv(dotenv_path=dotenv_path, override=False)
# 优先读取仓库根目录 .env再读取 backend/.env
# 已存在的环境变量(例如 Docker Compose 注入)保持最高优先级。
_load_env_file(REPO_DIR / ".env")
_load_env_file(BASE_DIR / ".env")
def _get_env(*names: str, default: str | None = None, allow_blank: bool = True) -> str | None:
for name in names:
value = os.getenv(name)
if value is None:
continue
value = value.strip() if isinstance(value, str) else value
if value == "" and not allow_blank:
continue
return value
return default
def _get_int_env(*names: str, default: int) -> int:
raw_value = _get_env(*names, default=str(default), allow_blank=False)
try:
return int(raw_value) if raw_value is not None else default
except (TypeError, ValueError):
return default
def _normalize_base_url(value: str | None) -> str:
normalized = str(value or "").strip().rstrip("/")
return normalized or "http://localhost"
def get_user_data_dir(user_id: int | str) -> Path: def get_user_data_dir(user_id: int | str) -> Path:
return USER_DIR / str(user_id) return USER_DIR / str(user_id)
@ -55,18 +88,18 @@ LEGACY_AVATAR_DIR.mkdir(exist_ok=True)
# 数据库配置 # 数据库配置
DATABASE_CONFIG = { DATABASE_CONFIG = {
'host': os.getenv('DB_HOST', '127.0.0.1'), 'host': _get_env('DB_HOST', 'MYSQL_HOST', default='127.0.0.1', allow_blank=False),
'user': os.getenv('DB_USER', 'root'), 'user': _get_env('DB_USER', 'MYSQL_USER', default='root', allow_blank=False),
'password': os.getenv('DB_PASSWORD', ''), 'password': _get_env('DB_PASSWORD', 'MYSQL_PASSWORD', default=''),
'database': os.getenv('DB_NAME', 'imeeting'), 'database': _get_env('DB_NAME', 'MYSQL_DATABASE', default='imeeting', allow_blank=False),
'port': int(os.getenv('DB_PORT', '3306')), 'port': _get_int_env('DB_PORT', 'MYSQL_PORT', default=3306),
'charset': 'utf8mb4' 'charset': 'utf8mb4'
} }
# API配置 # API配置
API_CONFIG = { API_CONFIG = {
'host': os.getenv('API_HOST', '0.0.0.0'), 'host': _get_env('API_HOST', default='0.0.0.0', allow_blank=False),
'port': int(os.getenv('API_PORT', '8000')) 'port': _get_int_env('API_PORT', default=8000)
} }
# 七牛云配置 # 七牛云配置
@ -77,31 +110,27 @@ API_CONFIG = {
# 应用配置 # 应用配置
APP_CONFIG = { APP_CONFIG = {
'base_url': os.getenv('BASE_URL', 'http://imeeting.unisspace.com') 'base_url': _normalize_base_url(_get_env('BASE_URL', default='http://localhost', allow_blank=False))
} }
# Redis配置 # Redis配置
REDIS_CONFIG = { REDIS_CONFIG = {
'host': os.getenv('REDIS_HOST', '127.0.0.1'), 'host': _get_env('REDIS_HOST', default='127.0.0.1', allow_blank=False),
'port': int(os.getenv('REDIS_PORT', '6379')), 'port': _get_int_env('REDIS_PORT', default=6379),
'db': int(os.getenv('REDIS_DB', '0')), 'db': _get_int_env('REDIS_DB', default=0),
'password': os.getenv('REDIS_PASSWORD', ''), 'password': _get_env('REDIS_PASSWORD', default=''),
'decode_responses': True 'decode_responses': True
} }
# Dashscope (Tongyi Qwen) API Key
QWEN_API_KEY = os.getenv('QWEN_API_KEY', 'sk-c2bf06ea56b4491ea3d1e37fdb472b8f')
# 转录轮询配置 - 用于 upload-audio-complete 接口 # 转录轮询配置 - 用于 upload-audio-complete 接口
TRANSCRIPTION_POLL_CONFIG = { TRANSCRIPTION_POLL_CONFIG = {
'poll_interval': int(os.getenv('TRANSCRIPTION_POLL_INTERVAL', '10')), # 轮询间隔10秒 'poll_interval': _get_int_env('TRANSCRIPTION_POLL_INTERVAL', default=10), # 轮询间隔10秒
'max_wait_time': int(os.getenv('TRANSCRIPTION_MAX_WAIT_TIME', '1800')), # 最大等待30分钟 'max_wait_time': _get_int_env('TRANSCRIPTION_MAX_WAIT_TIME', default=1800), # 最大等待30分钟
} }
# 后台任务配置 # 后台任务配置
BACKGROUND_TASK_CONFIG = { BACKGROUND_TASK_CONFIG = {
'summary_workers': int(os.getenv('SUMMARY_TASK_MAX_WORKERS', '2')), 'summary_workers': _get_int_env('SUMMARY_TASK_MAX_WORKERS', default=2),
'monitor_workers': int(os.getenv('MONITOR_TASK_MAX_WORKERS', '8')), 'monitor_workers': _get_int_env('MONITOR_TASK_MAX_WORKERS', default=8),
'transcription_status_cache_ttl': int(os.getenv('TRANSCRIPTION_STATUS_CACHE_TTL', '3')), 'transcription_status_cache_ttl': _get_int_env('TRANSCRIPTION_STATUS_CACHE_TTL', default=3),
} }

View File

@ -9,7 +9,7 @@ from http import HTTPStatus
import dashscope import dashscope
from dashscope.audio.asr import Transcription from dashscope.audio.asr import Transcription
from app.core.config import QWEN_API_KEY, REDIS_CONFIG, APP_CONFIG, BACKGROUND_TASK_CONFIG from app.core.config import REDIS_CONFIG, APP_CONFIG, BACKGROUND_TASK_CONFIG
from app.core.database import get_db_connection from app.core.database import get_db_connection
from app.services.system_config_service import SystemConfigService from app.services.system_config_service import SystemConfigService
@ -31,9 +31,8 @@ class AsyncTranscriptionService:
"""异步转录服务类""" """异步转录服务类"""
def __init__(self): def __init__(self):
dashscope.api_key = QWEN_API_KEY
self.redis_client = redis.Redis(**REDIS_CONFIG) self.redis_client = redis.Redis(**REDIS_CONFIG)
self.base_url = APP_CONFIG['base_url'] self.base_url = APP_CONFIG['base_url'].rstrip('/')
@staticmethod @staticmethod
def _create_requests_session(default_timeout: Optional[int] = None) -> requests.Session: def _create_requests_session(default_timeout: Optional[int] = None) -> requests.Session:
@ -53,7 +52,7 @@ class AsyncTranscriptionService:
@staticmethod @staticmethod
def _resolve_dashscope_api_key(audio_config: Optional[Dict[str, Any]] = None) -> str: def _resolve_dashscope_api_key(audio_config: Optional[Dict[str, Any]] = None) -> str:
api_key = (audio_config or {}).get("api_key") or QWEN_API_KEY api_key = (audio_config or {}).get("api_key")
if isinstance(api_key, str): if isinstance(api_key, str):
api_key = api_key.strip() api_key = api_key.strip()
if not api_key: if not api_key:
@ -194,7 +193,7 @@ class AsyncTranscriptionService:
cursor.close() cursor.close()
# 2. 构造完整的文件URL # 2. 构造完整的文件URL
file_url = f"{self.base_url}{audio_file_path}" file_url = f"{self.base_url}/{audio_file_path.lstrip('/')}"
# 获取音频模型配置 # 获取音频模型配置
audio_config = SystemConfigService.get_active_audio_model_config("asr") audio_config = SystemConfigService.get_active_audio_model_config("asr")

View File

@ -4,7 +4,6 @@ from typing import Optional, Dict, Generator, Any, List
import httpx import httpx
import app.core.config as config_module
from app.core.database import get_db_connection from app.core.database import get_db_connection
from app.services.system_config_service import SystemConfigService from app.services.system_config_service import SystemConfigService
@ -79,7 +78,7 @@ class LLMService:
endpoint_url = str(config.get("endpoint_url") or SystemConfigService.get_llm_endpoint_url() or "").strip() endpoint_url = str(config.get("endpoint_url") or SystemConfigService.get_llm_endpoint_url() or "").strip()
api_key = cls._normalize_api_key(config.get("api_key")) api_key = cls._normalize_api_key(config.get("api_key"))
if api_key is None: if api_key is None:
api_key = cls._normalize_api_key(SystemConfigService.get_llm_api_key(config_module.QWEN_API_KEY)) api_key = cls._normalize_api_key(SystemConfigService.get_llm_api_key())
default_model = SystemConfigService.get_llm_model_name() default_model = SystemConfigService.get_llm_model_name()
default_timeout = SystemConfigService.get_llm_timeout() default_timeout = SystemConfigService.get_llm_timeout()

View File

@ -93,9 +93,6 @@ services:
API_HOST: 0.0.0.0 API_HOST: 0.0.0.0
API_PORT: 8000 API_PORT: 8000
BASE_URL: ${BASE_URL:-http://localhost} BASE_URL: ${BASE_URL:-http://localhost}
# LLM配置
QWEN_API_KEY: ${QWEN_API_KEY}
# 后端不直接暴露端口通过nginx代理访问 # 后端不直接暴露端口通过nginx代理访问
ports: ports:
- "${BACKEND_PORT:-8000}:8000" - "${BACKEND_PORT:-8000}:8000"

View File

@ -71,7 +71,7 @@ check_env_files() {
if [ ! -f .env ]; then if [ ! -f .env ]; then
print_warning ".env 文件不存在,从模板创建..." print_warning ".env 文件不存在,从模板创建..."
cp .env.example .env cp .env.example .env
print_warning "请编辑 .env 文件配置访问端口、BASE_URL、QWEN_API_KEY 等参数" print_warning "请编辑 .env 文件配置访问端口、BASE_URL 等参数"
print_warning "按任意键继续,或 Ctrl+C 退出..." print_warning "按任意键继续,或 Ctrl+C 退出..."
read -n 1 -s read -n 1 -s
fi fi
@ -139,6 +139,12 @@ services:
REDIS_DB: "${REDIS_DB}" REDIS_DB: "${REDIS_DB}"
REDIS_PASSWORD: "${REDIS_PASSWORD}" REDIS_PASSWORD: "${REDIS_PASSWORD}"
EOF EOF
if [ -n "${BASE_URL:-}" ]; then
cat >> "$OVERRIDE_FILE" <<EOF
BASE_URL: "${BASE_URL}"
EOF
fi
} }
start_services() { start_services() {