From 9079d82729444dcb5f46ac0fe8f580fa0f1df16a Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Mon, 13 Apr 2026 17:27:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BA=86=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 32 +++++--- DOCKER_README.md | 38 +++++---- backend/.env.example | 27 ++++--- backend/app/api/endpoints/hot_words.py | 11 ++- backend/app/core/config.py | 79 +++++++++++++------ .../services/async_transcription_service.py | 9 +-- backend/app/services/llm_service.py | 3 +- docker-compose.yml | 3 - start-external.sh | 8 +- 9 files changed, 137 insertions(+), 73 deletions(-) diff --git a/.env.example b/.env.example index 0dbf11a..201e867 100644 --- a/.env.example +++ b/.env.example @@ -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_ROOT_PASSWORD=Unis@123 +# MySQL 初始化参数(Docker 内置 MySQL)。 +# 当后端也运行在 Docker 中时,数据库主机应为服务名 `mysql`。 +# 只有在宿主机直接运行后端时,才需要改成 127.0.0.1 或实际地址。 +MYSQL_HOST=mysql +MYSQL_ROOT_PASSWORD=change_this_password MYSQL_DATABASE=imeeting MYSQL_USER=imeeting -MYSQL_PASSWORD=Unis@123 +MYSQL_PASSWORD=change_this_password MYSQL_PORT=3306 # ==================== 缓存配置 ==================== -# Redis配置 +# Redis 初始化参数(Docker 内置 Redis)。 +# 当后端也运行在 Docker 中时,Redis 主机应为服务名 `redis`。 +# 只有在宿主机直接运行后端时,才需要改成 127.0.0.1 或实际地址。 +REDIS_HOST=redis REDIS_PORT=6379 -REDIS_PASSWORD=Unis@123 +REDIS_PASSWORD=change_this_password REDIS_DB=0 # ==================== 应用端口配置 ==================== @@ -17,10 +30,11 @@ REDIS_DB=0 HTTP_PORT=80 # ==================== 应用配置 ==================== -# 应用访问地址(用于生成外部链接、二维码等) -# - 直接访问: http://服务器IP -# - 域名访问: https://your-domain.com (需配置接入服务器Nginx) -BASE_URL=https://imeeting.unisspace.com +# 应用访问地址(用于生成外部链接、客户端下载链接,以及音频转录时提供给云端拉取音频文件的公网 URL) +# - 本地联调可先填写: http://localhost +# - 使用云端音频转录时,必须改成外部可访问的域名或公网地址,不能填写容器名、127.0.0.1 或内网地址 +# - 不要以 / 结尾,例如: https://your-domain.com +BASE_URL=https://your-domain.com # 前端API地址(通过Nginx代理访问后端) VITE_API_BASE_URL=/api diff --git a/DOCKER_README.md b/DOCKER_README.md index 05da3a7..66fdc1a 100644 --- a/DOCKER_README.md +++ b/DOCKER_README.md @@ -40,7 +40,7 @@ ```bash # 1. 复制并配置环境变量 cp .env.example .env -vim .env # 配置七牛云、LLM密钥等 +vim .env # 配置 BASE_URL、密码等 # 2. 一键启动 ./start.sh @@ -60,9 +60,7 @@ vim .env # 配置七牛云、LLM密钥等 ```bash # 1. 配置环境变量 cp .env.example .env -cp backend/.env.example backend/.env vim .env # 配置主环境变量 -vim backend/.env # 配置后端环境变量 # 2. 启动所有服务 docker-compose up -d @@ -89,17 +87,12 @@ docker-compose ps ### 必须配置项 -编辑 `.env` 文件,修改以下配置: +编辑根目录 `.env` 文件,修改以下配置: ```bash -# 七牛云存储(必填,否则无法上传文件) -QINIU_ACCESS_KEY=your_actual_access_key -QINIU_SECRET_KEY=your_actual_secret_key -QINIU_BUCKET=your_bucket_name -QINIU_DOMAIN=your_domain.clouddn.com - -# LLM API(必填,否则无法使用AI功能) -QWEN_API_KEY=your_actual_qwen_api_key +# Docker 一体化部署下,后端容器访问内置 MySQL/Redis 使用服务名 +MYSQL_HOST=mysql +REDIS_HOST=redis # 生产环境必改密码 MYSQL_ROOT_PASSWORD=change_this_password @@ -110,7 +103,8 @@ REDIS_PASSWORD=change_this_password ### 可选配置项 ```bash -# 应用访问地址(用于生成二维码等) +# 应用访问地址(用于生成外链,以及音频转录时给云端拉取音频文件) +# 如果使用云端音频转录,必须改成外部可访问的域名或公网地址,不能填写 localhost / 127.0.0.1 / 容器名 BASE_URL=http://localhost # 生产环境改为: https://your-domain.com # Nginx端口(默认80/443) @@ -122,6 +116,21 @@ HTTPS_PORT=443 # 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` @@ -260,8 +269,9 @@ curl http://localhost/docs |------|----------| | 端口被占用 | 修改.env中的HTTP_PORT | | 502错误 | 检查backend和frontend是否健康 | -| 数据库连接失败 | 检查backend/.env配置 | +| 数据库连接失败 | 一体化部署检查 `docker-compose.yml` 和根目录 `.env`,外接中间件模式检查 `backend/.env` | | 前端无法访问API | 检查VITE_API_BASE_URL配置 | +| 音频转录拉不到音频文件 | 检查 `BASE_URL` 是否为云端可访问的完整地址 | | 如何配置HTTPS | 参考本文档中的反向代理章节 | 详见:`DOCKER_README.md` 故障排查章节 diff --git a/backend/.env.example b/backend/.env.example index a497be6..1f675ec 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,27 +1,30 @@ # ==================== 数据库配置 ==================== -# Docker环境使用容器名称 -DB_HOST=10.100.51.51 -DB_USER=root -DB_PASSWORD=Unis@123 -DB_NAME=imeeting_dev +# 供“直接运行 backend”或“./start-external.sh 外接 MySQL/Redis”使用 +# 如果 backend 运行在 Docker 中,且数据库也在同一 Docker 网络,主机名应填服务名(如 mysql) +# 如果是宿主机直接运行 backend,再填写 127.0.0.1 或实际地址 +DB_HOST=127.0.0.1 +DB_USER=imeeting +DB_PASSWORD=change_this_password +DB_NAME=imeeting DB_PORT=3306 # ==================== Redis配置 ==================== -# Docker环境使用容器名称 -REDIS_HOST=10.100.51.51 +# 如果 backend 运行在 Docker 中,且 Redis 也在同一 Docker 网络,主机名应填服务名(如 redis) +# 如果是宿主机直接运行 backend,再填写 127.0.0.1 或实际地址 +REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_DB=0 -REDIS_PASSWORD=Unis@123 +REDIS_PASSWORD=change_this_password # ==================== API配置 ==================== API_HOST=0.0.0.0 API_PORT=8000 # ==================== 应用配置 ==================== -# 应用访问地址(用于生成外部链接、二维码等) -# 开发环境: http://localhost -# 生产环境: https://your-domain.com -BASE_URL=http://imeeting.unisspace.com +# 直接运行 backend 时可在这里配置 BASE_URL; +# Docker 容器部署时,优先使用仓库根目录 .env 中的 BASE_URL。 +# 使用云端音频转录时,必须填写云端可以访问到的公网地址,且不要以 / 结尾。 +# BASE_URL=https://your-domain.com # ==================== 转录轮询配置 ==================== diff --git a/backend/app/api/endpoints/hot_words.py b/backend/app/api/endpoints/hot_words.py index 3829eff..c3c4bdc 100644 --- a/backend/app/api/endpoints/hot_words.py +++ b/backend/app/api/endpoints/hot_words.py @@ -2,7 +2,6 @@ from fastapi import APIRouter, Depends, HTTPException from app.core.database import get_db_connection from app.core.auth import get_current_admin_user 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 pydantic import BaseModel from typing import Optional, List @@ -14,6 +13,14 @@ from http import HTTPStatus 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 ────────────────────────────────────────── 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)): """同步指定组到阿里云 DashScope""" try: - dashscope.api_key = QWEN_API_KEY + dashscope.api_key = _resolve_dashscope_api_key() with get_db_connection() as conn: cursor = conn.cursor(dictionary=True) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index d0a5738..f7ab33b 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -3,12 +3,9 @@ import json from pathlib import Path 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 +REPO_DIR = BASE_DIR.parent UPLOAD_DIR = BASE_DIR / "uploads" AUDIO_DIR = UPLOAD_DIR / "audio" TEMP_UPLOAD_DIR = UPLOAD_DIR / "temp_audio" @@ -22,6 +19,42 @@ VOICEPRINT_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: return USER_DIR / str(user_id) @@ -55,18 +88,18 @@ LEGACY_AVATAR_DIR.mkdir(exist_ok=True) # 数据库配置 DATABASE_CONFIG = { - 'host': os.getenv('DB_HOST', '127.0.0.1'), - 'user': os.getenv('DB_USER', 'root'), - 'password': os.getenv('DB_PASSWORD', ''), - 'database': os.getenv('DB_NAME', 'imeeting'), - 'port': int(os.getenv('DB_PORT', '3306')), + 'host': _get_env('DB_HOST', 'MYSQL_HOST', default='127.0.0.1', allow_blank=False), + 'user': _get_env('DB_USER', 'MYSQL_USER', default='root', allow_blank=False), + 'password': _get_env('DB_PASSWORD', 'MYSQL_PASSWORD', default=''), + 'database': _get_env('DB_NAME', 'MYSQL_DATABASE', default='imeeting', allow_blank=False), + 'port': _get_int_env('DB_PORT', 'MYSQL_PORT', default=3306), 'charset': 'utf8mb4' } # API配置 API_CONFIG = { - 'host': os.getenv('API_HOST', '0.0.0.0'), - 'port': int(os.getenv('API_PORT', '8000')) + 'host': _get_env('API_HOST', default='0.0.0.0', allow_blank=False), + 'port': _get_int_env('API_PORT', default=8000) } # 七牛云配置 @@ -77,31 +110,27 @@ API_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_CONFIG = { - 'host': os.getenv('REDIS_HOST', '127.0.0.1'), - 'port': int(os.getenv('REDIS_PORT', '6379')), - 'db': int(os.getenv('REDIS_DB', '0')), - 'password': os.getenv('REDIS_PASSWORD', ''), + 'host': _get_env('REDIS_HOST', default='127.0.0.1', allow_blank=False), + 'port': _get_int_env('REDIS_PORT', default=6379), + 'db': _get_int_env('REDIS_DB', default=0), + 'password': _get_env('REDIS_PASSWORD', default=''), 'decode_responses': True } -# Dashscope (Tongyi Qwen) API Key -QWEN_API_KEY = os.getenv('QWEN_API_KEY', 'sk-c2bf06ea56b4491ea3d1e37fdb472b8f') - # 转录轮询配置 - 用于 upload-audio-complete 接口 TRANSCRIPTION_POLL_CONFIG = { - 'poll_interval': int(os.getenv('TRANSCRIPTION_POLL_INTERVAL', '10')), # 轮询间隔:10秒 - 'max_wait_time': int(os.getenv('TRANSCRIPTION_MAX_WAIT_TIME', '1800')), # 最大等待:30分钟 + 'poll_interval': _get_int_env('TRANSCRIPTION_POLL_INTERVAL', default=10), # 轮询间隔:10秒 + 'max_wait_time': _get_int_env('TRANSCRIPTION_MAX_WAIT_TIME', default=1800), # 最大等待:30分钟 } # 后台任务配置 BACKGROUND_TASK_CONFIG = { - 'summary_workers': int(os.getenv('SUMMARY_TASK_MAX_WORKERS', '2')), - 'monitor_workers': int(os.getenv('MONITOR_TASK_MAX_WORKERS', '8')), - 'transcription_status_cache_ttl': int(os.getenv('TRANSCRIPTION_STATUS_CACHE_TTL', '3')), + 'summary_workers': _get_int_env('SUMMARY_TASK_MAX_WORKERS', default=2), + 'monitor_workers': _get_int_env('MONITOR_TASK_MAX_WORKERS', default=8), + 'transcription_status_cache_ttl': _get_int_env('TRANSCRIPTION_STATUS_CACHE_TTL', default=3), } - diff --git a/backend/app/services/async_transcription_service.py b/backend/app/services/async_transcription_service.py index b4ec948..9769f39 100644 --- a/backend/app/services/async_transcription_service.py +++ b/backend/app/services/async_transcription_service.py @@ -9,7 +9,7 @@ from http import HTTPStatus import dashscope 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.services.system_config_service import SystemConfigService @@ -31,9 +31,8 @@ class AsyncTranscriptionService: """异步转录服务类""" def __init__(self): - dashscope.api_key = QWEN_API_KEY self.redis_client = redis.Redis(**REDIS_CONFIG) - self.base_url = APP_CONFIG['base_url'] + self.base_url = APP_CONFIG['base_url'].rstrip('/') @staticmethod def _create_requests_session(default_timeout: Optional[int] = None) -> requests.Session: @@ -53,7 +52,7 @@ class AsyncTranscriptionService: @staticmethod 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): api_key = api_key.strip() if not api_key: @@ -194,7 +193,7 @@ class AsyncTranscriptionService: cursor.close() # 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") diff --git a/backend/app/services/llm_service.py b/backend/app/services/llm_service.py index ed311dc..1ea72c4 100644 --- a/backend/app/services/llm_service.py +++ b/backend/app/services/llm_service.py @@ -4,7 +4,6 @@ from typing import Optional, Dict, Generator, Any, List import httpx -import app.core.config as config_module from app.core.database import get_db_connection 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() api_key = cls._normalize_api_key(config.get("api_key")) 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_timeout = SystemConfigService.get_llm_timeout() diff --git a/docker-compose.yml b/docker-compose.yml index 7df8c2f..3709c7f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -93,9 +93,6 @@ services: API_HOST: 0.0.0.0 API_PORT: 8000 BASE_URL: ${BASE_URL:-http://localhost} - - # LLM配置 - QWEN_API_KEY: ${QWEN_API_KEY} # 后端不直接暴露端口,通过nginx代理访问 ports: - "${BACKEND_PORT:-8000}:8000" diff --git a/start-external.sh b/start-external.sh index 31a015f..7de1388 100755 --- a/start-external.sh +++ b/start-external.sh @@ -71,7 +71,7 @@ check_env_files() { if [ ! -f .env ]; then print_warning ".env 文件不存在,从模板创建..." cp .env.example .env - print_warning "请编辑 .env 文件,配置访问端口、BASE_URL、QWEN_API_KEY 等参数" + print_warning "请编辑 .env 文件,配置访问端口、BASE_URL 等参数" print_warning "按任意键继续,或 Ctrl+C 退出..." read -n 1 -s fi @@ -139,6 +139,12 @@ services: REDIS_DB: "${REDIS_DB}" REDIS_PASSWORD: "${REDIS_PASSWORD}" EOF + + if [ -n "${BASE_URL:-}" ]; then + cat >> "$OVERRIDE_FILE" <