v1.1.1
parent
d7507e811b
commit
2c505514a5
Binary file not shown.
|
Before Width: | Height: | Size: 89 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 225 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 167 KiB |
|
|
@ -2,26 +2,36 @@
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editors and local tooling
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.claude/
|
||||
.gemini-clipboard/
|
||||
.memsearch/
|
||||
frontend/.memsearch/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
backend/.env
|
||||
frontend/.env
|
||||
frontend/.env.local
|
||||
frontend/.env.development.local
|
||||
frontend/.env.test.local
|
||||
frontend/.env.production.local
|
||||
|
||||
# Docker / Data
|
||||
# Project data and local-only assets
|
||||
data/
|
||||
backups/
|
||||
logs_export/
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
frontend/node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
frontend/dist/
|
||||
frontend/logs/
|
||||
backend/uploads/
|
||||
backend/logs/
|
||||
backend/venv/
|
||||
backend/test/
|
||||
backend/sql/
|
||||
backend/scripts/
|
||||
资料/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
|
|
@ -29,13 +39,9 @@ __pycache__/
|
|||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
.env/
|
||||
.venv/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
develop-eggs/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
|
|
@ -43,25 +49,105 @@ lib/
|
|||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
share/python-wheels/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
backend/venv/
|
||||
backend/__pycache__/
|
||||
backend/logs/
|
||||
MANIFEST
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
.pyre/
|
||||
.pytype/
|
||||
cython_debug/
|
||||
.pdm.toml
|
||||
__pypackages__/
|
||||
.ipynb_checkpoints
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
instance/
|
||||
.webassets-cache
|
||||
.scrapy
|
||||
docs/_build/
|
||||
.pybuilder/
|
||||
target/
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
*.manifest
|
||||
*.spec
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Project specific
|
||||
backend/uploads/
|
||||
资料/
|
||||
# Node.js / frontend
|
||||
node_modules/
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
frontend/logs/
|
||||
jspm_packages/
|
||||
web_modules/
|
||||
bower_components/
|
||||
.npm
|
||||
.parcel-cache/
|
||||
.cache/
|
||||
.next/
|
||||
out/
|
||||
.nuxt/
|
||||
.vuepress/dist
|
||||
.temp
|
||||
.docusaurus
|
||||
.serverless/
|
||||
.fusebox/
|
||||
.dynamodb/
|
||||
.tern-port
|
||||
.vscode-test
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
.nyc_output
|
||||
coverage/
|
||||
*.lcov
|
||||
*.tsbuildinfo
|
||||
*.pid
|
||||
*.pid.lock
|
||||
*.seed
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
.node_repl_history
|
||||
*.tgz
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.yarn-integrity
|
||||
.pnp.*
|
||||
pids/
|
||||
logs/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Vibe Coding
|
||||
.memsearch
|
||||
/frontend/.memsearch
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ ENV/
|
|||
|
||||
# 用户上传文件(最重要!)
|
||||
uploads/
|
||||
test/
|
||||
sql/
|
||||
scripts/
|
||||
|
||||
# 测试和开发文件
|
||||
test/
|
||||
|
|
|
|||
|
|
@ -1,163 +0,0 @@
|
|||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
uploads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
|
|
@ -40,3 +40,8 @@ async def get_system_resources(current_user=Depends(get_current_admin_user)):
|
|||
@router.get("/admin/user-stats")
|
||||
async def get_user_stats(current_user=Depends(get_current_admin_user)):
|
||||
return await admin_dashboard_service.get_user_stats(current_user)
|
||||
|
||||
|
||||
@router.post("/admin/tasks/{task_type}/{task_id}/retry")
|
||||
async def retry_task(task_type: str, task_id: str, current_user=Depends(get_current_admin_user)):
|
||||
return await admin_dashboard_service.retry_task(task_type, task_id, current_user)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ from app.core.response import create_api_response
|
|||
from app.core.database import get_db_connection
|
||||
from app.services.jwt_service import jwt_service
|
||||
from app.core.config import AUDIO_DIR, REDIS_CONFIG
|
||||
from app.services.async_transcription_service import AsyncTranscriptionService
|
||||
from app.services.async_meeting_service import async_meeting_service
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
import os
|
||||
|
|
@ -15,6 +17,8 @@ redis_client = redis.Redis(**REDIS_CONFIG)
|
|||
AUDIO_FILE_EXTENSIONS = ('.wav', '.mp3', '.m4a', '.aac', '.flac', '.ogg', '.mpeg', '.mp4', '.webm')
|
||||
BYTES_TO_GB = 1024 ** 3
|
||||
|
||||
transcription_service = AsyncTranscriptionService()
|
||||
|
||||
|
||||
def _build_status_condition(status: str) -> str:
|
||||
"""构建任务状态查询条件"""
|
||||
|
|
@ -375,6 +379,112 @@ async def monitor_tasks(
|
|||
return create_api_response(code="500", message=f"获取任务监控数据失败: {str(e)}")
|
||||
|
||||
|
||||
def _parse_optional_int(value):
|
||||
if value in (None, "", "None"):
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
async def retry_task(task_type: str, task_id: str, current_user=None):
|
||||
"""重试或恢复后台任务。"""
|
||||
try:
|
||||
normalized_type = (task_type or "").strip().lower()
|
||||
if normalized_type == "summary":
|
||||
return _retry_summary_task(task_id)
|
||||
if normalized_type == "transcription":
|
||||
return _retry_transcription_task(task_id)
|
||||
return create_api_response(code="400", message="不支持的任务类型")
|
||||
except Exception as e:
|
||||
print(f"重试任务失败: {e}")
|
||||
return create_api_response(code="500", message=f"重试任务失败: {str(e)}")
|
||||
|
||||
|
||||
def _retry_summary_task(task_id: str):
|
||||
task_data = async_meeting_service._get_task_from_db(task_id)
|
||||
if not task_data:
|
||||
return create_api_response(code="404", message="总结任务不存在")
|
||||
|
||||
status = str(task_data.get("status") or "").lower()
|
||||
meeting_id = _parse_optional_int(task_data.get("meeting_id"))
|
||||
if not meeting_id:
|
||||
return create_api_response(code="400", message="总结任务缺少关联会议")
|
||||
|
||||
if status in {"pending", "processing"}:
|
||||
async_meeting_service._resume_task_if_needed(task_id, task_data)
|
||||
status_info = async_meeting_service.get_task_status(task_id)
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="总结任务已尝试恢复",
|
||||
data={"task_id": task_id, "status": status_info.get("status"), "progress": status_info.get("progress", 0)},
|
||||
)
|
||||
|
||||
if status == "completed":
|
||||
return create_api_response(code="400", message="总结任务已完成,无需重试")
|
||||
|
||||
prompt_id = _parse_optional_int(task_data.get("prompt_id"))
|
||||
user_prompt = "" if task_data.get("user_prompt") in (None, "None") else str(task_data.get("user_prompt"))
|
||||
new_task_id, _ = async_meeting_service.enqueue_summary_generation(
|
||||
meeting_id,
|
||||
user_prompt=user_prompt,
|
||||
prompt_id=prompt_id,
|
||||
model_code=None,
|
||||
)
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="总结任务已重新提交",
|
||||
data={"task_id": new_task_id, "previous_task_id": task_id, "status": "pending", "meeting_id": meeting_id},
|
||||
)
|
||||
|
||||
|
||||
def _retry_transcription_task(task_id: str):
|
||||
task_data = transcription_service._get_task_from_db(task_id)
|
||||
if not task_data:
|
||||
return create_api_response(code="404", message="转录任务不存在")
|
||||
|
||||
status = str(task_data.get("status") or "").lower()
|
||||
meeting_id = _parse_optional_int(task_data.get("meeting_id"))
|
||||
if not meeting_id:
|
||||
return create_api_response(code="400", message="转录任务缺少关联会议")
|
||||
|
||||
if status in {"pending", "processing"}:
|
||||
status_info = transcription_service.get_task_status(task_id)
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="转录任务状态已刷新",
|
||||
data={"task_id": task_id, "status": status_info.get("status"), "progress": status_info.get("progress", 0)},
|
||||
)
|
||||
|
||||
if status == "completed":
|
||||
return create_api_response(code="400", message="转录任务已完成,无需重试")
|
||||
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT file_path FROM audio_files WHERE meeting_id = %s LIMIT 1", (meeting_id,))
|
||||
audio_file = cursor.fetchone()
|
||||
cursor.execute("SELECT prompt_id FROM meetings WHERE meeting_id = %s LIMIT 1", (meeting_id,))
|
||||
meeting = cursor.fetchone()
|
||||
cursor.close()
|
||||
|
||||
if not audio_file or not audio_file.get("file_path"):
|
||||
return create_api_response(code="400", message="会议缺少可用音频,无法重试转录")
|
||||
|
||||
new_task_id = transcription_service.start_transcription(meeting_id, audio_file["file_path"])
|
||||
async_meeting_service.enqueue_transcription_monitor(
|
||||
meeting_id,
|
||||
new_task_id,
|
||||
_parse_optional_int((meeting or {}).get("prompt_id")),
|
||||
None,
|
||||
)
|
||||
return create_api_response(
|
||||
code="200",
|
||||
message="转录任务已重新提交",
|
||||
data={"task_id": new_task_id, "previous_task_id": task_id, "status": "pending", "meeting_id": meeting_id},
|
||||
)
|
||||
|
||||
|
||||
async def get_system_resources(current_user=None):
|
||||
"""获取服务器资源使用情况"""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -561,6 +561,8 @@ class AsyncMeetingService:
|
|||
task_data = self._get_task_from_db(task_id)
|
||||
if not task_data:
|
||||
return {'task_id': task_id, 'status': 'not_found', 'error_message': 'Task not found'}
|
||||
self._resume_task_if_needed(task_id, task_data)
|
||||
task_data = self.redis_client.hgetall(f"llm_task:{task_id}") or task_data
|
||||
|
||||
return {
|
||||
'task_id': task_id,
|
||||
|
|
@ -577,6 +579,33 @@ class AsyncMeetingService:
|
|||
print(f"Error getting task status: {e}")
|
||||
return {'task_id': task_id, 'status': 'error', 'error_message': str(e)}
|
||||
|
||||
def _resume_task_if_needed(self, task_id: str, task_data: Dict[str, Any]) -> None:
|
||||
"""恢复服务重启后丢失在内存中的总结任务。"""
|
||||
try:
|
||||
status = str(task_data.get('status') or '').lower()
|
||||
if status not in {'pending', 'processing'}:
|
||||
return
|
||||
|
||||
restored_data = {
|
||||
'task_id': task_id,
|
||||
'meeting_id': str(task_data.get('meeting_id') or ''),
|
||||
'user_prompt': '' if task_data.get('user_prompt') in (None, 'None') else str(task_data.get('user_prompt')),
|
||||
'prompt_id': '' if task_data.get('prompt_id') in (None, 'None') else str(task_data.get('prompt_id')),
|
||||
'model_code': '' if task_data.get('model_code') in (None, 'None') else str(task_data.get('model_code')),
|
||||
'status': status,
|
||||
'progress': str(task_data.get('progress') or 0),
|
||||
'created_at': task_data.get('created_at') or datetime.now().isoformat(),
|
||||
'updated_at': datetime.now().isoformat(),
|
||||
'message': '任务恢复中,准备继续执行...',
|
||||
}
|
||||
self.redis_client.hset(f"llm_task:{task_id}", mapping=restored_data)
|
||||
self.redis_client.expire(f"llm_task:{task_id}", 86400)
|
||||
|
||||
submitted = summary_task_runner.submit(f"meeting-summary:{task_id}", self._process_task, task_id)
|
||||
print(f"[LLM Task Recovery] task_id={task_id}, status={status}, submitted={submitted}")
|
||||
except Exception as e:
|
||||
print(f"Error resuming summary task {task_id}: {e}")
|
||||
|
||||
def get_meeting_llm_tasks(self, meeting_id: int) -> List[Dict[str, Any]]:
|
||||
"""获取会议的所有LLM任务"""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -349,6 +349,7 @@ class AsyncTranscriptionService:
|
|||
updated_at = cached_status.get('updated_at') or updated_at
|
||||
else:
|
||||
# 2. 查询外部API获取状态
|
||||
paraformer_response = None
|
||||
try:
|
||||
audio_config = SystemConfigService.get_active_audio_model_config("asr")
|
||||
request_options = self._build_dashscope_request_options(audio_config)
|
||||
|
|
@ -364,12 +365,22 @@ class AsyncTranscriptionService:
|
|||
error_message = None
|
||||
|
||||
except Exception as e:
|
||||
current_status = 'failed'
|
||||
progress = 0
|
||||
error_message = f"Error fetching status from provider: {e}"
|
||||
# 云侧状态查询抖动不应直接把任务打成 failed,
|
||||
# 保持当前非终态并等待下一轮轮询重试。
|
||||
current_status = task_data.get('status') or 'processing'
|
||||
progress = int(task_data.get('progress') or 0)
|
||||
error_message = None
|
||||
print(
|
||||
f"Transient provider status fetch error for task {business_task_id}: {e}. "
|
||||
f"Keeping status={current_status}, progress={progress}"
|
||||
)
|
||||
|
||||
# 3. 如果任务完成,处理结果
|
||||
if current_status == 'completed' and paraformer_response.output.get('results'):
|
||||
if (
|
||||
current_status == 'completed'
|
||||
and paraformer_response is not None
|
||||
and paraformer_response.output.get('results')
|
||||
):
|
||||
db_task_status = self._get_task_status_from_db(business_task_id)
|
||||
if db_task_status != 'completed':
|
||||
self._update_task_status_in_db(business_task_id, 'completed', 100, None)
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
from pathlib import Path
|
||||
import shutil
|
||||
import re
|
||||
|
||||
from app.core.database import get_db_connection
|
||||
import app.core.config as config_module
|
||||
|
||||
OLD_AVATAR_PREFIX = "/uploads/user/avatar/"
|
||||
NEW_AVATAR_PREFIX = "/uploads/user/"
|
||||
OLD_VOICEPRINT_PREFIX = "uploads/user/voiceprint/"
|
||||
NEW_VOICEPRINT_PREFIX = "uploads/user/"
|
||||
|
||||
|
||||
def move_tree_contents(old_dir: Path, new_dir: Path):
|
||||
if not old_dir.exists():
|
||||
return False
|
||||
new_dir.mkdir(parents=True, exist_ok=True)
|
||||
moved = False
|
||||
for item in old_dir.iterdir():
|
||||
target = new_dir / item.name
|
||||
if item.resolve() == target.resolve():
|
||||
continue
|
||||
if target.exists():
|
||||
continue
|
||||
shutil.move(str(item), str(target))
|
||||
moved = True
|
||||
return moved
|
||||
|
||||
|
||||
def migrate_avatar_files():
|
||||
legacy_root = config_module.LEGACY_AVATAR_DIR
|
||||
if not legacy_root.exists():
|
||||
return
|
||||
for user_dir in legacy_root.iterdir():
|
||||
if not user_dir.is_dir():
|
||||
continue
|
||||
target_dir = config_module.get_user_avatar_dir(user_dir.name)
|
||||
move_tree_contents(user_dir, target_dir)
|
||||
|
||||
|
||||
def migrate_voiceprint_files():
|
||||
legacy_root = config_module.LEGACY_VOICEPRINT_DIR
|
||||
if not legacy_root.exists():
|
||||
return
|
||||
for user_dir in legacy_root.iterdir():
|
||||
if not user_dir.is_dir():
|
||||
continue
|
||||
target_dir = config_module.get_user_voiceprint_dir(user_dir.name)
|
||||
move_tree_contents(user_dir, target_dir)
|
||||
|
||||
|
||||
def migrate_avatar_urls(cursor):
|
||||
cursor.execute("SELECT user_id, avatar_url FROM sys_users WHERE avatar_url LIKE %s", (f"{OLD_AVATAR_PREFIX}%",))
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
avatar_url = row["avatar_url"]
|
||||
if not avatar_url:
|
||||
continue
|
||||
match = re.match(r"^/uploads/user/avatar/(\d+)/(.*)$", avatar_url)
|
||||
if not match:
|
||||
continue
|
||||
user_id, filename = match.groups()
|
||||
new_url = f"/uploads/user/{user_id}/avatar/{filename}"
|
||||
cursor.execute("UPDATE sys_users SET avatar_url = %s WHERE user_id = %s", (new_url, row["user_id"]))
|
||||
|
||||
|
||||
def migrate_voiceprint_paths(cursor):
|
||||
try:
|
||||
cursor.execute("SELECT vp_id, file_path FROM user_voiceprint WHERE file_path LIKE %s", (f"{OLD_VOICEPRINT_PREFIX}%",))
|
||||
except Exception:
|
||||
return
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
file_path = row["file_path"]
|
||||
if not file_path:
|
||||
continue
|
||||
match = re.match(r"^uploads/user/voiceprint/(\d+)/(.*)$", file_path)
|
||||
if not match:
|
||||
continue
|
||||
user_id, filename = match.groups()
|
||||
new_path = f"uploads/user/{user_id}/voiceprint/{filename}"
|
||||
cursor.execute("UPDATE user_voiceprint SET file_path = %s WHERE vp_id = %s", (new_path, row["vp_id"]))
|
||||
|
||||
|
||||
def main():
|
||||
migrate_avatar_files()
|
||||
migrate_voiceprint_files()
|
||||
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
migrate_avatar_urls(cursor)
|
||||
migrate_voiceprint_paths(cursor)
|
||||
connection.commit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pymysql
|
||||
|
||||
|
||||
def parse_env_file(env_path: Path):
|
||||
data = {}
|
||||
for raw in env_path.read_text(encoding='utf-8').splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' not in line:
|
||||
continue
|
||||
k, v = line.split('=', 1)
|
||||
data[k.strip()] = v.strip()
|
||||
return data
|
||||
|
||||
|
||||
def split_sql_statements(sql_text: str):
|
||||
statements = []
|
||||
buf = []
|
||||
in_single = False
|
||||
in_double = False
|
||||
in_line_comment = False
|
||||
in_block_comment = False
|
||||
|
||||
i = 0
|
||||
while i < len(sql_text):
|
||||
ch = sql_text[i]
|
||||
nxt = sql_text[i + 1] if i + 1 < len(sql_text) else ''
|
||||
|
||||
if in_line_comment:
|
||||
if ch == '\n':
|
||||
in_line_comment = False
|
||||
buf.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if in_block_comment:
|
||||
if ch == '*' and nxt == '/':
|
||||
in_block_comment = False
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if not in_single and not in_double:
|
||||
if ch == '-' and nxt == '-':
|
||||
in_line_comment = True
|
||||
i += 2
|
||||
continue
|
||||
if ch == '#':
|
||||
in_line_comment = True
|
||||
i += 1
|
||||
continue
|
||||
if ch == '/' and nxt == '*':
|
||||
in_block_comment = True
|
||||
i += 2
|
||||
continue
|
||||
|
||||
if ch == "'" and not in_double:
|
||||
in_single = not in_single
|
||||
buf.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == '"' and not in_single:
|
||||
in_double = not in_double
|
||||
buf.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == ';' and not in_single and not in_double:
|
||||
stmt = ''.join(buf).strip()
|
||||
if stmt:
|
||||
statements.append(stmt)
|
||||
buf = []
|
||||
i += 1
|
||||
continue
|
||||
|
||||
buf.append(ch)
|
||||
i += 1
|
||||
|
||||
tail = ''.join(buf).strip()
|
||||
if tail:
|
||||
statements.append(tail)
|
||||
return statements
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Run SQL migration from file')
|
||||
parser.add_argument('--env', default='backend/.env', help='Path to .env file')
|
||||
parser.add_argument('--sql', required=True, help='Path to SQL file')
|
||||
args = parser.parse_args()
|
||||
|
||||
env = parse_env_file(Path(args.env))
|
||||
sql_path = Path(args.sql)
|
||||
if not sql_path.exists():
|
||||
print(f'[ERROR] SQL file not found: {sql_path}')
|
||||
return 1
|
||||
|
||||
sql_text = sql_path.read_text(encoding='utf-8')
|
||||
statements = split_sql_statements(sql_text)
|
||||
if not statements:
|
||||
print('[ERROR] No SQL statements found')
|
||||
return 1
|
||||
|
||||
conn = pymysql.connect(
|
||||
host=env.get('DB_HOST', '127.0.0.1'),
|
||||
port=int(env.get('DB_PORT', '3306')),
|
||||
user=env.get('DB_USER', 'root'),
|
||||
password=env.get('DB_PASSWORD', ''),
|
||||
database=env.get('DB_NAME', ''),
|
||||
charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
autocommit=False,
|
||||
)
|
||||
|
||||
# duplicate column/index tolerated for idempotency rerun
|
||||
tolerated_errnos = {1060, 1061, 1831}
|
||||
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
print(f'[INFO] Running {len(statements)} statements from {sql_path}')
|
||||
for idx, stmt in enumerate(statements, start=1):
|
||||
normalized = re.sub(r'\s+', ' ', stmt).strip()
|
||||
head = normalized[:120]
|
||||
try:
|
||||
cur.execute(stmt)
|
||||
print(f'[OK] {idx:03d}: {head}')
|
||||
except pymysql.MySQLError as e:
|
||||
if e.args and e.args[0] in tolerated_errnos:
|
||||
print(f'[SKIP] {idx:03d}: errno={e.args[0]} {e.args[1]} | {head}')
|
||||
continue
|
||||
conn.rollback()
|
||||
print(f'[FAIL] {idx:03d}: errno={e.args[0] if e.args else "?"} {e}')
|
||||
print(f'[STMT] {head}')
|
||||
return 2
|
||||
|
||||
conn.commit()
|
||||
print('[INFO] Migration committed successfully')
|
||||
|
||||
checks = [
|
||||
"SELECT COUNT(*) AS cnt FROM menus",
|
||||
"SELECT COUNT(*) AS cnt FROM role_menu_permissions",
|
||||
"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='menus' AND COLUMN_NAME IN ('menu_level','tree_path','is_visible')",
|
||||
"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='role_menu_permissions' AND COLUMN_NAME IN ('granted_by','granted_at')",
|
||||
]
|
||||
for q in checks:
|
||||
cur.execute(q)
|
||||
row = cur.fetchone()
|
||||
print(f'[CHECK] {q} => {row}')
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import mysql.connector
|
||||
|
||||
|
||||
def parse_env_file(env_path: Path):
|
||||
data = {}
|
||||
for raw in env_path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
data[k.strip()] = v.strip()
|
||||
return data
|
||||
|
||||
|
||||
def split_sql_statements(sql_text: str):
|
||||
statements = []
|
||||
buf = []
|
||||
in_single = False
|
||||
in_double = False
|
||||
in_line_comment = False
|
||||
in_block_comment = False
|
||||
i = 0
|
||||
|
||||
while i < len(sql_text):
|
||||
ch = sql_text[i]
|
||||
nxt = sql_text[i + 1] if i + 1 < len(sql_text) else ""
|
||||
|
||||
if in_line_comment:
|
||||
if ch == "\n":
|
||||
in_line_comment = False
|
||||
buf.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if in_block_comment:
|
||||
if ch == "*" and nxt == "/":
|
||||
in_block_comment = False
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if not in_single and not in_double:
|
||||
if ch == "-" and nxt == "-":
|
||||
in_line_comment = True
|
||||
i += 2
|
||||
continue
|
||||
if ch == "#":
|
||||
in_line_comment = True
|
||||
i += 1
|
||||
continue
|
||||
if ch == "/" and nxt == "*":
|
||||
in_block_comment = True
|
||||
i += 2
|
||||
continue
|
||||
|
||||
if ch == "'" and not in_double:
|
||||
in_single = not in_single
|
||||
buf.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == '"' and not in_single:
|
||||
in_double = not in_double
|
||||
buf.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == ";" and not in_single and not in_double:
|
||||
stmt = "".join(buf).strip()
|
||||
if stmt:
|
||||
statements.append(stmt)
|
||||
buf = []
|
||||
i += 1
|
||||
continue
|
||||
|
||||
buf.append(ch)
|
||||
i += 1
|
||||
|
||||
tail = "".join(buf).strip()
|
||||
if tail:
|
||||
statements.append(tail)
|
||||
return statements
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Run SQL migration using mysql-connector")
|
||||
parser.add_argument("--env", default="backend/.env", help="Path to .env file")
|
||||
parser.add_argument("--sql", required=True, help="Path to SQL file")
|
||||
args = parser.parse_args()
|
||||
|
||||
env = parse_env_file(Path(args.env))
|
||||
sql_path = Path(args.sql)
|
||||
if not sql_path.exists():
|
||||
print(f"[ERROR] SQL file not found: {sql_path}")
|
||||
return 1
|
||||
|
||||
sql_text = sql_path.read_text(encoding="utf-8")
|
||||
statements = split_sql_statements(sql_text)
|
||||
if not statements:
|
||||
print("[ERROR] No SQL statements found")
|
||||
return 1
|
||||
|
||||
conn = mysql.connector.connect(
|
||||
host=env.get("DB_HOST", "127.0.0.1"),
|
||||
port=int(env.get("DB_PORT", "3306")),
|
||||
user=env.get("DB_USER", "root"),
|
||||
password=env.get("DB_PASSWORD", ""),
|
||||
database=env.get("DB_NAME", ""),
|
||||
)
|
||||
|
||||
try:
|
||||
cur = conn.cursor(dictionary=True)
|
||||
print(f"[INFO] Running {len(statements)} statements from {sql_path}")
|
||||
for idx, stmt in enumerate(statements, start=1):
|
||||
head = " ".join(stmt.split())[:120]
|
||||
cur.execute(stmt)
|
||||
if cur.with_rows:
|
||||
cur.fetchall()
|
||||
print(f"[OK] {idx:03d}: {head}")
|
||||
conn.commit()
|
||||
print("[INFO] Migration committed successfully")
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"[FAIL] {e}")
|
||||
return 2
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
# 客户端管理 - 专用终端类型添加说明
|
||||
|
||||
## 概述
|
||||
|
||||
本次更新在客户端管理系统中添加了"专用终端"(terminal)大类型,支持 Android 专用终端和单片机(MCU)平台。
|
||||
|
||||
## 数据库变更
|
||||
|
||||
### 1. 修改表结构
|
||||
|
||||
执行 SQL 文件:`add_dedicated_terminal.sql`
|
||||
|
||||
```bash
|
||||
mysql -u [username] -p [database_name] < backend/sql/add_dedicated_terminal.sql
|
||||
```
|
||||
|
||||
**变更内容:**
|
||||
- 修改 `client_downloads` 表的 `platform_type` 枚举,添加 `terminal` 类型
|
||||
- 插入两条示例数据:
|
||||
- Android 专用终端(platform_type: `terminal`, platform_name: `android`)
|
||||
- 单片机固件(platform_type: `terminal`, platform_name: `mcu`)
|
||||
|
||||
### 2. 新的平台类型
|
||||
|
||||
| platform_type | platform_name | 说明 |
|
||||
|--------------|--------------|------|
|
||||
| terminal | android | Android 专用终端 |
|
||||
| terminal | mcu | 单片机(MCU)固件 |
|
||||
|
||||
## API 接口变更
|
||||
|
||||
### 1. 新增接口:通过平台类型和平台名称获取最新版本
|
||||
|
||||
**接口路径:** `GET /api/downloads/latest/by-platform`
|
||||
|
||||
**请求参数:**
|
||||
- `platform_type` (string, required): 平台类型 (mobile, desktop, terminal)
|
||||
- `platform_name` (string, required): 具体平台名称
|
||||
|
||||
**示例请求:**
|
||||
```bash
|
||||
# 获取 Android 专用终端最新版本
|
||||
curl "http://localhost:8000/api/downloads/latest/by-platform?platform_type=terminal&platform_name=android"
|
||||
|
||||
# 获取单片机固件最新版本
|
||||
curl "http://localhost:8000/api/downloads/latest/by-platform?platform_type=terminal&platform_name=mcu"
|
||||
```
|
||||
|
||||
**返回示例:**
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"message": "获取成功",
|
||||
"data": {
|
||||
"id": 7,
|
||||
"platform_type": "terminal",
|
||||
"platform_name": "android",
|
||||
"version": "1.0.0",
|
||||
"version_code": 1000,
|
||||
"download_url": "https://download.imeeting.com/terminals/android/iMeeting-Terminal-1.0.0.apk",
|
||||
"file_size": 25165824,
|
||||
"release_notes": "专用终端初始版本\n- 支持专用硬件集成\n- 优化的录音功能\n- 低功耗模式\n- 自动上传同步",
|
||||
"is_active": true,
|
||||
"is_latest": true,
|
||||
"min_system_version": "Android 5.0",
|
||||
"created_at": "2025-01-15T10:00:00",
|
||||
"updated_at": "2025-01-15T10:00:00",
|
||||
"created_by": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 更新接口:获取所有平台最新版本
|
||||
|
||||
**接口路径:** `GET /api/downloads/latest`
|
||||
|
||||
**变更:** 返回数据中新增 `terminal` 字段
|
||||
|
||||
**返回示例:**
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"message": "获取成功",
|
||||
"data": {
|
||||
"mobile": [...],
|
||||
"desktop": [...],
|
||||
"terminal": [
|
||||
{
|
||||
"id": 7,
|
||||
"platform_type": "terminal",
|
||||
"platform_name": "android",
|
||||
"version": "1.0.0",
|
||||
...
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"platform_type": "terminal",
|
||||
"platform_name": "mcu",
|
||||
"version": "1.0.0",
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 已有接口说明
|
||||
|
||||
**原有接口:** `GET /api/downloads/{platform_name}/latest`
|
||||
|
||||
- 此接口标记为【已废弃】,建议使用新接口 `/downloads/latest/by-platform`
|
||||
- 原因:只通过 `platform_name` 查询可能产生歧义(如 mobile 的 android 和 terminal 的 android)
|
||||
- 保留此接口是为了向后兼容,但新开发应使用新接口
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景 1:专用终端设备版本检查
|
||||
|
||||
专用终端设备(如会议室固定录音设备、单片机硬件)启动时检查更新:
|
||||
|
||||
```javascript
|
||||
// Android 专用终端
|
||||
const response = await fetch(
|
||||
'/api/downloads/latest/by-platform?platform_type=terminal&platform_name=android'
|
||||
);
|
||||
const { data } = await response.json();
|
||||
|
||||
if (data.version_code > currentVersionCode) {
|
||||
// 发现新版本,提示更新
|
||||
showUpdateDialog(data);
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 2:后台管理界面展示
|
||||
|
||||
管理员查看所有终端版本:
|
||||
|
||||
```javascript
|
||||
const response = await fetch('/api/downloads?platform_type=terminal');
|
||||
const { data } = await response.json();
|
||||
|
||||
// data.clients 包含所有 terminal 类型的客户端版本
|
||||
renderClientList(data.clients);
|
||||
```
|
||||
|
||||
### 场景 3:固件更新服务器
|
||||
|
||||
单片机设备定期轮询更新:
|
||||
|
||||
```c
|
||||
// MCU 固件代码示例
|
||||
char url[] = "http://api.imeeting.com/downloads/latest/by-platform?platform_type=terminal&platform_name=mcu";
|
||||
http_get(url, response_buffer);
|
||||
|
||||
// 解析 JSON 获取 download_url 和 version_code
|
||||
if (new_version > FIRMWARE_VERSION) {
|
||||
download_and_update(download_url);
|
||||
}
|
||||
```
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 数据库测试
|
||||
```sql
|
||||
-- 验证表结构修改
|
||||
DESCRIBE client_downloads;
|
||||
|
||||
-- 验证数据插入
|
||||
SELECT * FROM client_downloads WHERE platform_type = 'terminal';
|
||||
```
|
||||
|
||||
### 2. API 测试
|
||||
```bash
|
||||
# 测试新接口
|
||||
curl "http://localhost:8000/api/downloads/latest/by-platform?platform_type=terminal&platform_name=android"
|
||||
|
||||
curl "http://localhost:8000/api/downloads/latest/by-platform?platform_type=terminal&platform_name=mcu"
|
||||
|
||||
# 测试获取所有最新版本
|
||||
curl "http://localhost:8000/api/downloads/latest"
|
||||
|
||||
# 测试列表接口
|
||||
curl "http://localhost:8000/api/downloads?platform_type=terminal"
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **执行 SQL 前请备份数据库**
|
||||
2. **ENUM 类型修改**:ALTER TABLE 会修改表结构,请在低峰期执行
|
||||
3. **新接口优先**:建议所有新开发使用 `/downloads/latest/by-platform` 接口
|
||||
4. **版本管理**:上传新版本时记得设置 `is_latest=TRUE` 并将同平台旧版本设为 `FALSE`
|
||||
5. **platform_name 唯一性**:如果 mobile 和 terminal 都有 android,建议:
|
||||
- mobile 的保持 `android`
|
||||
- terminal 的改为 `android_terminal` 或其他区分名称
|
||||
- 或者始终使用新接口同时传递 platform_type 和 platform_name
|
||||
|
||||
## 文件清单
|
||||
|
||||
- `backend/sql/add_dedicated_terminal.sql` - 数据库迁移 SQL
|
||||
- `backend/app/api/endpoints/client_downloads.py` - API 接口代码
|
||||
- `backend/sql/README_terminal_update.md` - 本说明文档
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
-- 专用终端设备表
|
||||
CREATE TABLE IF NOT EXISTS `terminals` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`imei` varchar(64) NOT NULL COMMENT 'IMEI号(设备唯一标识)',
|
||||
`terminal_name` varchar(100) DEFAULT NULL COMMENT '终端名称/设备别名',
|
||||
`terminal_type` varchar(50) NOT NULL COMMENT '终端类型(关联dict_data.dict_code)',
|
||||
`description` varchar(500) DEFAULT NULL COMMENT '终端说明/备注',
|
||||
|
||||
-- 状态管理
|
||||
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '启用状态: 1-启用, 0-停用',
|
||||
`is_activated` tinyint(1) NOT NULL DEFAULT '0' COMMENT '激活状态: 1-已激活, 0-未激活',
|
||||
`activated_at` datetime DEFAULT NULL COMMENT '激活时间',
|
||||
|
||||
-- 运维监控字段
|
||||
`firmware_version` varchar(50) DEFAULT NULL COMMENT '当前固件版本',
|
||||
`last_online_at` datetime DEFAULT NULL COMMENT '最后在线/心跳时间',
|
||||
`ip_address` varchar(50) DEFAULT NULL COMMENT '最近一次连接IP',
|
||||
`mac_address` varchar(64) DEFAULT NULL COMMENT 'MAC地址',
|
||||
|
||||
-- 审计字段
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '录入时间',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`created_by` int(11) DEFAULT NULL COMMENT '录入人ID',
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_imei` (`imei`),
|
||||
KEY `idx_terminal_type` (`terminal_type`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='专用终端设备表';
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS `hot_words` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`text` VARCHAR(255) NOT NULL COMMENT '热词内容',
|
||||
`weight` INT NOT NULL DEFAULT 4 COMMENT '词汇权重 (1-10)',
|
||||
`lang` VARCHAR(20) NOT NULL DEFAULT 'zh' COMMENT '语言 (zh/en)',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态 (1:启用, 0:禁用)',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_text_lang` (`text`, `lang`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统语音识别热词表';
|
||||
|
||||
-- 预留存储 Vocabulary ID 的配置项(如果不想用字典表存储配置,也可以在系统配置表中增加)
|
||||
INSERT INTO `dict_data` (dict_type, dict_code, parent_code, label_cn, status)
|
||||
VALUES ('system_config', 'asr_vocabulary_id', 'ROOT', '阿里云ASR热词表ID', 1)
|
||||
ON DUPLICATE KEY UPDATE label_cn='阿里云ASR热词表ID';
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
-- ===================================================================
|
||||
-- 菜单权限系统数据库迁移脚本
|
||||
-- 创建日期: 2025-12-10
|
||||
-- 说明: 添加 menus 表和 role_menu_permissions 表,实现基于角色的多级菜单权限管理
|
||||
-- ===================================================================
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for menus
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `menus`;
|
||||
CREATE TABLE `menus` (
|
||||
`menu_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
|
||||
`menu_code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单代码(唯一标识)',
|
||||
`menu_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称',
|
||||
`menu_icon` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '菜单图标标识',
|
||||
`menu_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '菜单URL/路由',
|
||||
`menu_type` enum('action','link','divider') COLLATE utf8mb4_unicode_ci DEFAULT 'action' COMMENT '菜单类型: action-操作/link-链接/divider-分隔符',
|
||||
`parent_id` int(11) DEFAULT NULL COMMENT '父菜单ID(用于层级菜单)',
|
||||
`menu_level` tinyint(3) NOT NULL DEFAULT 1 COMMENT '菜单层级(根节点为1)',
|
||||
`tree_path` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '树路径(如 /3/6)',
|
||||
`sort_order` int(11) DEFAULT 0 COMMENT '排序顺序',
|
||||
`is_active` tinyint(1) DEFAULT 1 COMMENT '是否启用: 1-启用, 0-禁用',
|
||||
`is_visible` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否在侧边菜单显示',
|
||||
`description` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '菜单描述',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`menu_id`),
|
||||
UNIQUE KEY `uk_menu_code` (`menu_code`),
|
||||
KEY `idx_parent_id` (`parent_id`),
|
||||
KEY `idx_menu_level` (`menu_level`),
|
||||
KEY `idx_tree_path` (`tree_path`),
|
||||
KEY `idx_is_active` (`is_active`),
|
||||
KEY `idx_is_visible` (`is_visible`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统菜单表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for role_menu_permissions
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `role_menu_permissions`;
|
||||
CREATE TABLE `role_menu_permissions` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '权限ID',
|
||||
`role_id` int(11) NOT NULL COMMENT '角色ID',
|
||||
`menu_id` int(11) NOT NULL COMMENT '菜单ID',
|
||||
`granted_by` int(11) DEFAULT NULL COMMENT '授权操作人ID',
|
||||
`granted_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '授权时间',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_role_menu` (`role_id`,`menu_id`),
|
||||
KEY `idx_role_id` (`role_id`),
|
||||
KEY `idx_menu_id` (`menu_id`),
|
||||
KEY `idx_granted_by` (`granted_by`),
|
||||
KEY `idx_granted_at` (`granted_at`),
|
||||
CONSTRAINT `fk_rmp_role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`role_id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_rmp_menu_id` FOREIGN KEY (`menu_id`) REFERENCES `menus` (`menu_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色菜单权限映射表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 初始化菜单数据
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
|
||||
-- 一级菜单
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`)
|
||||
VALUES
|
||||
('account_settings', '账户设置', 'UserCog', '/account-settings', 'link', NULL, 1, NULL, 1, 1, 1, '管理个人账户信息'),
|
||||
('prompt_management', '提示词仓库', 'BookText', '/prompt-management', 'link', NULL, 1, NULL, 2, 1, 1, '管理AI提示词模版'),
|
||||
('platform_admin', '平台管理', 'Shield', '/admin/management/user-management', 'link', NULL, 1, NULL, 3, 1, 1, '平台管理员后台'),
|
||||
('logout', '退出登录', 'LogOut', NULL, 'action', NULL, 1, NULL, 99, 1, 1, '退出当前账号');
|
||||
|
||||
-- 二级菜单(挂载到平台管理)
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`)
|
||||
SELECT 'user_management', '用户管理', 'Users', '/admin/management/user-management', 'link', menu_id, 2, NULL, 1, 1, 1, '账号、角色、密码重置'
|
||||
FROM `menus` WHERE `menu_code` = 'platform_admin';
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`)
|
||||
SELECT 'permission_management', '权限管理', 'KeyRound', '/admin/management/permission-management', 'link', menu_id, 2, NULL, 2, 1, 1, '菜单与角色授权矩阵'
|
||||
FROM `menus` WHERE `menu_code` = 'platform_admin';
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`)
|
||||
SELECT 'dict_management', '字典管理', 'BookMarked', '/admin/management/dict-management', 'link', menu_id, 2, NULL, 3, 1, 1, '码表、平台类型、扩展属性'
|
||||
FROM `menus` WHERE `menu_code` = 'platform_admin';
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`)
|
||||
SELECT 'hot_word_management', '热词管理', 'Text', '/admin/management/hot-word-management', 'link', menu_id, 2, NULL, 4, 1, 1, 'ASR 热词与同步'
|
||||
FROM `menus` WHERE `menu_code` = 'platform_admin';
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`)
|
||||
SELECT 'client_management', '客户端管理', 'Smartphone', '/admin/management/client-management', 'link', menu_id, 2, NULL, 5, 1, 1, '版本、下载地址、发布状态'
|
||||
FROM `menus` WHERE `menu_code` = 'platform_admin';
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`)
|
||||
SELECT 'external_app_management', '外部应用管理', 'AppWindow', '/admin/management/external-app-management', 'link', menu_id, 2, NULL, 6, 1, 1, '外部系统入口与图标配置'
|
||||
FROM `menus` WHERE `menu_code` = 'platform_admin';
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`)
|
||||
SELECT 'terminal_management', '终端管理', 'Monitor', '/admin/management/terminal-management', 'link', menu_id, 2, NULL, 7, 1, 1, '专用设备、激活和绑定状态'
|
||||
FROM `menus` WHERE `menu_code` = 'platform_admin';
|
||||
|
||||
-- 回填路径
|
||||
UPDATE `menus` SET `tree_path` = CONCAT('/', `menu_id`) WHERE `parent_id` IS NULL;
|
||||
UPDATE `menus` c JOIN `menus` p ON c.`parent_id` = p.`menu_id`
|
||||
SET c.`tree_path` = CONCAT(p.`tree_path`, '/', c.`menu_id`);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
-- 初始化角色权限数据
|
||||
-- 注意:角色表已存在,role_id=1为平台管理员,role_id=2为普通用户
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
|
||||
-- 平台管理员(role_id=1)拥有所有菜单权限
|
||||
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`)
|
||||
SELECT 1, menu_id FROM `menus` WHERE is_active = 1;
|
||||
|
||||
-- 普通用户(role_id=2)排除平台管理与其二级子菜单
|
||||
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`)
|
||||
SELECT 2, menu_id FROM `menus`
|
||||
WHERE is_active = 1
|
||||
AND menu_code NOT IN (
|
||||
'platform_admin',
|
||||
'user_management',
|
||||
'permission_management',
|
||||
'dict_management',
|
||||
'hot_word_management',
|
||||
'client_management',
|
||||
'external_app_management',
|
||||
'terminal_management'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
-- 为 llm_tasks 表添加 prompt_id 列,用于支持自定义模版选择功能
|
||||
-- 执行日期:2025-12-08
|
||||
|
||||
ALTER TABLE `llm_tasks`
|
||||
ADD COLUMN `prompt_id` int(11) DEFAULT NULL COMMENT '提示词模版ID' AFTER `user_prompt`,
|
||||
ADD KEY `idx_prompt_id` (`prompt_id`);
|
||||
|
||||
-- 说明:
|
||||
-- 1. prompt_id 允许为 NULL,表示使用默认模版
|
||||
-- 2. 添加索引以优化查询性能
|
||||
-- 3. 不添加外键约束,因为 prompts 表中的记录可能被删除,我们希望保留历史任务记录
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
-- 添加 task_type 字典数据
|
||||
-- 用于会议任务和知识库任务的分类
|
||||
|
||||
INSERT INTO dict_data (dict_type, dict_code, parent_code, label_cn, label_en, sort_order, status, extension_attr) VALUES
|
||||
('task_type', 'MEETING_TASK', 'ROOT', '会议任务', 'Meeting Task', 1, 1, NULL),
|
||||
('task_type', 'KNOWLEDGE_TASK', 'ROOT', '知识库任务', 'Knowledge Task', 2, 1, NULL)
|
||||
ON DUPLICATE KEY UPDATE label_cn=VALUES(label_cn), label_en=VALUES(label_en);
|
||||
|
||||
-- 查看结果
|
||||
SELECT * FROM dict_data WHERE dict_type='task_type';
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
-- 客户端下载管理表
|
||||
CREATE TABLE IF NOT EXISTS client_downloads (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
platform_type ENUM('mobile', 'desktop') NOT NULL COMMENT '平台类型:mobile-移动端, desktop-桌面端',
|
||||
platform_name VARCHAR(50) NOT NULL COMMENT '具体平台:ios, android, windows, mac_intel, mac_m, linux',
|
||||
version VARCHAR(50) NOT NULL COMMENT '版本号,如: 1.0.0',
|
||||
version_code INT NOT NULL DEFAULT 1 COMMENT '版本代码,用于版本比较',
|
||||
download_url TEXT NOT NULL COMMENT '下载链接',
|
||||
file_size BIGINT COMMENT '文件大小(字节)',
|
||||
release_notes TEXT COMMENT '更新说明',
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用',
|
||||
is_latest BOOLEAN DEFAULT FALSE COMMENT '是否为最新版本',
|
||||
min_system_version VARCHAR(50) COMMENT '最低系统版本要求',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
created_by INT COMMENT '创建人ID',
|
||||
FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL,
|
||||
INDEX idx_platform (platform_type, platform_name),
|
||||
INDEX idx_version (version_code),
|
||||
INDEX idx_active (is_active, is_latest)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户端下载管理表';
|
||||
|
||||
-- 插入初始数据(示例版本)
|
||||
INSERT INTO client_downloads (
|
||||
platform_type,
|
||||
platform_name,
|
||||
version,
|
||||
version_code,
|
||||
download_url,
|
||||
file_size,
|
||||
release_notes,
|
||||
is_active,
|
||||
is_latest,
|
||||
min_system_version,
|
||||
created_by
|
||||
) VALUES
|
||||
-- iOS 客户端
|
||||
(
|
||||
'mobile',
|
||||
'ios',
|
||||
'1.0.0',
|
||||
1000,
|
||||
'https://apps.apple.com/app/imeeting/id123456789',
|
||||
52428800, -- 50MB
|
||||
'初始版本发布
|
||||
- 支持会议录音
|
||||
- 支持实时转录
|
||||
- 支持会议摘要查看',
|
||||
TRUE,
|
||||
TRUE,
|
||||
'iOS 13.0',
|
||||
1
|
||||
),
|
||||
|
||||
-- Android 客户端
|
||||
(
|
||||
'mobile',
|
||||
'android',
|
||||
'1.0.0',
|
||||
1000,
|
||||
'https://play.google.com/store/apps/details?id=com.imeeting.app',
|
||||
45088768, -- 43MB
|
||||
'初始版本发布
|
||||
- 支持会议录音
|
||||
- 支持实时转录
|
||||
- 支持会议摘要查看',
|
||||
TRUE,
|
||||
TRUE,
|
||||
'Android 8.0',
|
||||
1
|
||||
),
|
||||
|
||||
-- Windows 客户端
|
||||
(
|
||||
'desktop',
|
||||
'windows',
|
||||
'1.0.0',
|
||||
1000,
|
||||
'https://download.imeeting.com/clients/windows/iMeeting-1.0.0-Setup.exe',
|
||||
104857600, -- 100MB
|
||||
'初始版本发布
|
||||
- 完整的会议管理功能
|
||||
- 高清音频录制
|
||||
- AI智能转录
|
||||
- 知识库管理',
|
||||
TRUE,
|
||||
TRUE,
|
||||
'Windows 10 (64-bit)',
|
||||
1
|
||||
),
|
||||
|
||||
-- Mac Intel 客户端
|
||||
(
|
||||
'desktop',
|
||||
'mac_intel',
|
||||
'1.0.0',
|
||||
1000,
|
||||
'https://download.imeeting.com/clients/mac/iMeeting-1.0.0-Intel.dmg',
|
||||
94371840, -- 90MB
|
||||
'初始版本发布
|
||||
- 完整的会议管理功能
|
||||
- 高清音频录制
|
||||
- AI智能转录
|
||||
- 知识库管理',
|
||||
TRUE,
|
||||
TRUE,
|
||||
'macOS 10.15 Catalina',
|
||||
1
|
||||
),
|
||||
|
||||
-- Mac M系列 客户端
|
||||
(
|
||||
'desktop',
|
||||
'mac_m',
|
||||
'1.0.0',
|
||||
1000,
|
||||
'https://download.imeeting.com/clients/mac/iMeeting-1.0.0-AppleSilicon.dmg',
|
||||
83886080, -- 80MB
|
||||
'初始版本发布
|
||||
- 完整的会议管理功能
|
||||
- 高清音频录制
|
||||
- AI智能转录
|
||||
- 知识库管理
|
||||
- 原生支持Apple Silicon',
|
||||
TRUE,
|
||||
TRUE,
|
||||
'macOS 11.0 Big Sur',
|
||||
1
|
||||
),
|
||||
|
||||
-- Linux 客户端
|
||||
(
|
||||
'desktop',
|
||||
'linux',
|
||||
'1.0.0',
|
||||
1000,
|
||||
'https://download.imeeting.com/clients/linux/iMeeting-1.0.0-x64.AppImage',
|
||||
98566144, -- 94MB
|
||||
'初始版本发布
|
||||
- 完整的会议管理功能
|
||||
- 高清音频录制
|
||||
- AI智能转录
|
||||
- 知识库管理
|
||||
- 支持主流Linux发行版',
|
||||
TRUE,
|
||||
TRUE,
|
||||
'Ubuntu 20.04 / Debian 10 / Fedora 32 或更高版本',
|
||||
1
|
||||
);
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
-- 客户端下载管理表
|
||||
-- 保留 platform_type 和 platform_name 字段以兼容旧终端
|
||||
-- 新增 platform_code 关联 dict_data 表的码表数据
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `client_downloads` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`platform_type` VARCHAR(50) NULL COMMENT '平台类型(兼容旧版:mobile, desktop, terminal)',
|
||||
`platform_name` VARCHAR(50) NULL COMMENT '平台名称(兼容旧版:ios, android, windows等)',
|
||||
`platform_code` VARCHAR(64) NOT NULL COMMENT '平台编码(关联 dict_data.dict_code)',
|
||||
`version` VARCHAR(50) NOT NULL COMMENT '版本号(如 1.0.0)',
|
||||
`version_code` INT NOT NULL COMMENT '版本号数值(用于版本比较)',
|
||||
`download_url` VARCHAR(512) NOT NULL COMMENT '下载链接',
|
||||
`file_size` BIGINT NULL COMMENT '文件大小(bytes)',
|
||||
`release_notes` TEXT NULL COMMENT '更新说明',
|
||||
`is_active` BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否启用',
|
||||
`is_latest` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否为最新版本',
|
||||
`min_system_version` VARCHAR(50) NULL COMMENT '最低系统版本要求',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`created_by` INT NULL COMMENT '创建者用户ID',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_platform_code` (`platform_code`),
|
||||
INDEX `idx_platform_type_name` (`platform_type`, `platform_name`),
|
||||
INDEX `idx_is_latest` (`is_latest`),
|
||||
INDEX `idx_is_active` (`is_active`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户端下载管理表';
|
||||
|
||||
-- 插入测试数据示例(包含新旧字段映射)
|
||||
-- 旧终端使用 platform_type + platform_name
|
||||
-- 新终端使用 platform_code
|
||||
-- INSERT INTO client_downloads (platform_type, platform_name, platform_code, version, version_code, download_url, file_size, release_notes, is_active, is_latest, min_system_version, created_by)
|
||||
-- VALUES
|
||||
-- ('desktop', 'windows', 'WIN', '1.0.0', 100, 'https://download.example.com/imeeting-win-1.0.0.exe', 52428800, '首个正式版本', TRUE, TRUE, 'Windows 10', 1),
|
||||
-- ('desktop', 'mac', 'MAC', '1.0.0', 100, 'https://download.example.com/imeeting-mac-1.0.0.dmg', 48234496, '首个正式版本', TRUE, TRUE, 'macOS 11.0', 1),
|
||||
-- ('mobile', 'ios', 'IOS', '1.0.0', 100, 'https://apps.apple.com/app/imeeting', 45088768, '首个正式版本', TRUE, TRUE, 'iOS 13.0', 1),
|
||||
-- ('mobile', 'android', 'ANDROID', '1.0.0', 100, 'https://download.example.com/imeeting-android-1.0.0.apk', 38797312, '首个正式版本', TRUE, TRUE, 'Android 8.0', 1);
|
||||
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
-- iMeeting 数据库初始化脚本 (MySQL 5.7 兼容)
|
||||
-- 基于 project.md v3
|
||||
|
||||
-- 设置数据库和字符集
|
||||
-- 请在使用前手动创建数据库: CREATE DATABASE imeeting CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
-- USE imeeting;
|
||||
|
||||
-- 删除已存在的表 (用于重新执行脚本)
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
DROP TABLE IF EXISTS `meeting_summaries`, `transcript_segments`, `audio_files`, `attachments`, `attendees`, `meetings`, `users`, `tags`;
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- 1. 创建表结构
|
||||
|
||||
-- 用户表
|
||||
CREATE TABLE `users` (
|
||||
`user_id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`username` VARCHAR(50) UNIQUE NOT NULL,
|
||||
`caption` VARCHAR(50) NOT NULL,
|
||||
`email` VARCHAR(100) UNIQUE NOT NULL,
|
||||
`password_hash` VARCHAR(255) NOT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 会议表
|
||||
CREATE TABLE `meetings` (
|
||||
`meeting_id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` INT, -- 会议创建者
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`meeting_time` TIMESTAMP NULL,
|
||||
`summary` TEXT, -- 以Markdown格式存储
|
||||
`tags` VARCHAR(1024) DEFAULT NULL, -- 以逗号分隔的标签字符串
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 参会人表 (关联用户)
|
||||
CREATE TABLE `attendees` (
|
||||
`attendee_id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`meeting_id` INT,
|
||||
`user_id` INT,
|
||||
UNIQUE KEY `uk_meeting_user` (`meeting_id`, `user_id`) -- 确保同一用户在同一会议中只出现一次
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 会议材料附件表
|
||||
CREATE TABLE `attachments` (
|
||||
`attachment_id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`meeting_id` INT,
|
||||
`file_name` VARCHAR(255) NOT NULL,
|
||||
`file_path` VARCHAR(512) NOT NULL, -- 存储路径或URL
|
||||
`file_type` VARCHAR(100),
|
||||
`uploaded_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 音频文件与处理任务表
|
||||
CREATE TABLE `audio_files` (
|
||||
`audio_id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`meeting_id` INT,
|
||||
`file_name` VARCHAR(255),
|
||||
`file_path` VARCHAR(512) NOT NULL,
|
||||
`file_size` BIGINT DEFAULT NULL,
|
||||
`upload_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`processing_status` VARCHAR(20) DEFAULT 'uploaded', -- 'uploaded', 'processing', 'completed', 'failed'
|
||||
`error_message` TEXT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 转录任务表
|
||||
CREATE TABLE `transcript_tasks` (
|
||||
`task_id` VARCHAR(100) PRIMARY KEY,
|
||||
`paraformer_task_id` VARCHAR(100) DEFAULT NULL,
|
||||
`meeting_id` INT NOT NULL,
|
||||
`status` ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending',
|
||||
`progress` INT DEFAULT 0, -- 0-100 进度百分比
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`completed_at` TIMESTAMP NULL,
|
||||
`error_message` TEXT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 转录内容表 (核心)
|
||||
CREATE TABLE `transcript_segments` (
|
||||
`segment_id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`meeting_id` INT,
|
||||
`speaker_id` INT, -- 解析出来的人员ID
|
||||
`speaker_tag` VARCHAR(50) NOT NULL, -- e.g., "Speaker A", "李雷"
|
||||
`start_time_ms` INT NOT NULL, -- 音频开始时间(毫秒)
|
||||
`end_time_ms` INT NOT NULL, -- 音频结束时间(毫秒)
|
||||
`text_content` TEXT NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 标签表 (用于标签快速检索和颜色管理)
|
||||
CREATE TABLE `tags` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`color` varchar(7) DEFAULT '#409EFF',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `tag_name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 2. 插入测试数据
|
||||
|
||||
-- 插入用户 (4名)
|
||||
INSERT INTO `users` (`username`, `caption`, `email`, `password_hash`) VALUES
|
||||
('user1', 'alice', 'alice@example.com', 'hashed_password_1'),
|
||||
('user2', 'bob', 'bob@example.com', 'hashed_password_2'),
|
||||
('user3', 'charlie', 'charlie@example.com', 'hashed_password_3'),
|
||||
('user4', 'david', 'david@example.com', 'hashed_password_4');
|
||||
|
||||
-- 插入会议 (6条)
|
||||
INSERT INTO `meetings` (`user_id`, `title`, `meeting_time`, `summary`, `tags`) VALUES
|
||||
(1, 'Q3产品战略规划会', '2025-07-28 10:00:00', '# Q3产品战略规划会
|
||||
|
||||
## 核心议题
|
||||
- **目标**: 确定Q3主要产品迭代方向。
|
||||
- **讨论**: AI功能集成方案。
|
||||
|
||||
## 结论
|
||||
- 推进AI摘要功能开发。', '产品,重要'),
|
||||
(2, '“智慧大脑”项目技术评审', '2025-07-29 14:30:00', '技术方案已通过,部分细节待优化。', '技术'),
|
||||
(1, '营销团队周会', '2025-07-30 09:00:00', '回顾上周数据,制定本周计划。', '营销'),
|
||||
(3, '关于新版UI的设计评审', '2025-07-30 11:00:00', '## UI评审
|
||||
- **优点**: 简洁、现代。
|
||||
- **待办**: 调整登录页按钮颜色。', '设计'),
|
||||
(4, '年度财务报告初审', '2025-07-31 15:00:00', NULL, NULL),
|
||||
(2, '服务器架构升级讨论', '2025-08-01 16:00:00', '初步同意采用微服务架构。', '技术,重要');
|
||||
|
||||
-- 插入参会人
|
||||
-- 会议1: Alice, Bob, Charlie
|
||||
INSERT INTO `attendees` (`meeting_id`, `user_id`) VALUES (1, 1), (1, 2), (1, 3);
|
||||
-- 会议2: Bob, David
|
||||
INSERT INTO `attendees` (`meeting_id`, `user_id`) VALUES (2, 2), (2, 4);
|
||||
-- 会议3: Alice, Charlie
|
||||
INSERT INTO `attendees` (`meeting_id`, `user_id`) VALUES (3, 1), (3, 3);
|
||||
-- 会议4: Charlie, Alice, David
|
||||
INSERT INTO `attendees` (`meeting_id`, `user_id`) VALUES (4, 3), (4, 1), (4, 4);
|
||||
-- 会议5: David, Bob
|
||||
INSERT INTO `attendees` (`meeting_id`, `user_id`) VALUES (5, 4), (5, 2);
|
||||
-- 会议6: Bob, Charlie, David
|
||||
INSERT INTO `attendees` (`meeting_id`, `user_id`) VALUES (6, 2), (6, 3), (6, 4);
|
||||
|
||||
-- 插入会议材料
|
||||
INSERT INTO `attachments` (`meeting_id`, `file_name`, `file_path`, `file_type`) VALUES
|
||||
(1, 'Q3产品规划.pptx', '/uploads/meeting_1/q3_plan.pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'),
|
||||
(2, '技术方案V2.pdf', '/uploads/meeting_2/tech_spec_v2.pdf', 'application/pdf');
|
||||
|
||||
-- 插入音频文件记录
|
||||
INSERT INTO `audio_files` (`meeting_id`, `file_name`, `file_path`, `file_size`, `processing_status`) VALUES
|
||||
(1, 'meeting_1_audio.mp3', '/uploads/audio/1/meeting_1_audio.mp3', 15728640, 'completed'),
|
||||
(2, 'meeting_2_audio.wav', '/uploads/audio/2/meeting_2_audio.wav', 23456780, 'processing'),
|
||||
(3, 'meeting_3_audio.m4a', '/uploads/audio/3/meeting_3_audio.m4a', 18923456, 'uploaded'),
|
||||
(4, 'meeting_4_audio.mp3', '/uploads/audio/4/meeting_4_audio.mp3', 12345678, 'failed');
|
||||
|
||||
-- 插入转录任务记录
|
||||
INSERT INTO `transcript_tasks` (`task_id`, `meeting_id`, `status`, `progress`, `created_at`) VALUES
|
||||
('task-uuid-1', 1, 'completed', 100, '2025-07-28 10:05:00'),
|
||||
('task-uuid-2', 2, 'processing', 45, '2025-07-29 14:35:00'),
|
||||
('task-uuid-4', 4, 'failed', 0, '2025-07-30 11:05:00');
|
||||
|
||||
-- 插入转录内容 (为会议1)
|
||||
INSERT INTO `transcript_segments` (`meeting_id`, `speaker_id`, `speaker_tag`, `start_time_ms`, `end_time_ms`, `text_content`) VALUES
|
||||
(1, 0, '发言人 0', 5200, 9800, '好的,我们开始今天Q3的战略规划会。'),
|
||||
(1, 1, '发言人 1', 10100, 15500, '我先同步一下上个季度的数据,我们的用户增长了20%,主要来自于新推出的移动端。'),
|
||||
(1, 0, '发言人 0', 16000, 21300, '非常好。这个季度,我希望我们能重点讨论一下AI功能的集成,特别是会议摘要这部分。'),
|
||||
(1, 2, '发言人 2', 21800, 28000, '我同意,自动摘要可以极大地提升用户体验,我这边已经做了一些初步的技术调研。');
|
||||
|
||||
-- 插入标签
|
||||
INSERT INTO `tags` (`name`, `color`) VALUES
|
||||
('产品', '#409EFF'),
|
||||
('技术', '#67C23A'),
|
||||
('营销', '#E6A23C'),
|
||||
('设计', '#F56C6C'),
|
||||
('重要', '#909399');
|
||||
|
||||
-- 3. 添加外键约束
|
||||
ALTER TABLE `meetings` ADD CONSTRAINT `fk_meetings_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `attendees` ADD CONSTRAINT `fk_attendees_meeting` FOREIGN KEY (`meeting_id`) REFERENCES `meetings`(`meeting_id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `attendees` ADD CONSTRAINT `fk_attendees_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `attachments` ADD CONSTRAINT `fk_attachments_meeting` FOREIGN KEY (`meeting_id`) REFERENCES `meetings`(`meeting_id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `audio_files` ADD CONSTRAINT `fk_audio_files_meeting` FOREIGN KEY (`meeting_id`) REFERENCES `meetings`(`meeting_id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `transcript_tasks` ADD CONSTRAINT `fk_transcript_tasks_meeting` FOREIGN KEY (`meeting_id`) REFERENCES `meetings`(`meeting_id`) ON DELETE CASCADE;
|
||||
ALTER TABLE `transcript_segments` ADD CONSTRAINT `fk_transcript_segments_meeting` FOREIGN KEY (`meeting_id`) REFERENCES `meetings`(`meeting_id`) ON DELETE CASCADE;
|
||||
|
||||
-- 4. 添加索引优化查询性能
|
||||
-- audio_files 表索引
|
||||
ALTER TABLE `audio_files` ADD INDEX `idx_meeting_id` (`meeting_id`);
|
||||
ALTER TABLE `audio_files` ADD INDEX `idx_task_id` (`task_id`);
|
||||
ALTER TABLE `audio_files` ADD INDEX `idx_processing_status` (`processing_status`);
|
||||
|
||||
-- transcript_tasks 表索引
|
||||
ALTER TABLE `transcript_tasks` ADD INDEX `idx_meeting_id` (`meeting_id`);
|
||||
ALTER TABLE `transcript_tasks` ADD INDEX `idx_status` (`status`);
|
||||
ALTER TABLE `transcript_tasks` ADD INDEX `idx_created_at` (`created_at`);
|
||||
|
||||
-- transcript_segments 表索引
|
||||
ALTER TABLE `transcript_segments` ADD INDEX `idx_meeting_id` (`meeting_id`);
|
||||
ALTER TABLE `transcript_segments` ADD INDEX `idx_speaker_id` (`speaker_id`);
|
||||
ALTER TABLE `transcript_segments` ADD INDEX `idx_start_time` (`start_time_ms`);
|
||||
|
||||
-- meetings 表索引
|
||||
ALTER TABLE `meetings` ADD INDEX `idx_user_id` (`user_id`);
|
||||
ALTER TABLE `meetings` ADD INDEX `idx_meeting_time` (`meeting_time`);
|
||||
ALTER TABLE `meetings` ADD INDEX `idx_created_at` (`created_at`);
|
||||
ALTER TABLE `meetings` ADD INDEX `idx_tags` (`tags`(255));
|
||||
|
||||
-- attendees 表索引
|
||||
ALTER TABLE `attendees` ADD INDEX `idx_meeting_id` (`meeting_id`);
|
||||
ALTER TABLE `attendees` ADD INDEX `idx_user_id` (`user_id`);
|
||||
|
||||
-- attachments 表索引
|
||||
ALTER TABLE `attachments` ADD INDEX `idx_meeting_id` (`meeting_id`);
|
||||
|
||||
-- tags 表索引
|
||||
ALTER TABLE `tags` ADD INDEX `idx_name` (`name`);
|
||||
|
||||
-- 脚本结束
|
||||
SELECT '数据库初始化脚本 (MySQL) 执行完毕。';
|
||||
|
||||
-- Knowledge Base Tables
|
||||
CREATE TABLE IF NOT EXISTS `prompts` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(255) NOT NULL UNIQUE COMMENT '提示词名称,保持唯一以方便管理',
|
||||
`tags` VARCHAR(255) COMMENT '标签,用于分类和搜索,多个标签用逗号分隔',
|
||||
`content` TEXT NOT NULL COMMENT '完整的提示词内容',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
|
||||
) COMMENT='用于存储AI总结的提示词模板';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `knowledge_bases` (
|
||||
`kb_id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`title` VARCHAR(255) NOT NULL COMMENT '标题',
|
||||
`content` TEXT NULL COMMENT '生成的知识库内容 (Markdown格式)',
|
||||
`creator_id` INT NOT NULL COMMENT '创建者用户ID',
|
||||
`is_shared` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否为共享知识库',
|
||||
`source_meeting_ids` VARCHAR(255) NULL COMMENT '内容来源的会议ID列表 (逗号分隔)',
|
||||
`tags` VARCHAR(255) NULL COMMENT '逗号分隔的标签',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`creator_id`) REFERENCES `users`(`user_id`) ON DELETE CASCADE
|
||||
) COMMENT='知识库条目表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `knowledge_base_tasks` (
|
||||
`task_id` VARCHAR(100) PRIMARY KEY COMMENT '业务任务唯一ID (UUID)',
|
||||
`user_id` INT NOT NULL COMMENT '发起任务的用户ID',
|
||||
`kb_id` INT NOT NULL COMMENT '关联的知识库条目ID',
|
||||
`user_prompt` TEXT NULL COMMENT '用户输入的提示词',
|
||||
`status` ENUM('pending', 'processing', 'completed', 'failed') NOT NULL DEFAULT 'pending' COMMENT '任务状态',
|
||||
`progress` INT DEFAULT 0 COMMENT '任务进度百分比 (0-100)',
|
||||
`error_message` TEXT NULL COMMENT '任务失败时的错误信息',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`completed_at` TIMESTAMP NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`kb_id`) REFERENCES `knowledge_bases`(`kb_id`) ON DELETE CASCADE
|
||||
) COMMENT='知识库生成任务表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `prompt_config` (
|
||||
`config_id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`task_name` VARCHAR(100) UNIQUE NOT NULL COMMENT '任务名称',
|
||||
`prompt_id` INT NOT NULL COMMENT '关联的提示词模版ID',
|
||||
FOREIGN KEY (`prompt_id`) REFERENCES `prompts`(`id`)
|
||||
) COMMENT='提示词配置表';
|
||||
|
||||
-- Initial data for prompt_config
|
||||
INSERT INTO `prompt_config` (`task_name`, `prompt_id`) VALUES ('LLM_TASK', 1);
|
||||
INSERT INTO `prompt_config` (`task_name`, `prompt_id`) VALUES ('KNOWLEDGE_TASK', 2);
|
||||
|
||||
-- You might need to insert prompts with id=1 and id=2 into the `prompts` table for this to work.
|
||||
-- Example:
|
||||
-- INSERT INTO `prompts` (`id`, `name`, `content`) VALUES (1, 'Default Meeting Summary', 'Please summarize the following meeting transcript...');
|
||||
-- INSERT INTO `prompts` (`id`, `name`, `content`) VALUES (2, 'Default Knowledge Base Generation', 'Please generate a knowledge base article from the following text...');
|
||||
File diff suppressed because it is too large
Load Diff
25548
backend/sql/imeeting.sql
25548
backend/sql/imeeting.sql
File diff suppressed because one or more lines are too long
|
|
@ -1,67 +0,0 @@
|
|||
-- 提示词表改造迁移脚本
|
||||
-- 将 prompt_config 表的功能整合到 prompts 表
|
||||
|
||||
-- 步骤1: 添加新字段
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN task_type ENUM('MEETING_TASK', 'KNOWLEDGE_TASK')
|
||||
COMMENT '任务类型:MEETING_TASK-会议任务, KNOWLEDGE_TASK-知识库任务' AFTER name;
|
||||
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN is_default BOOLEAN NOT NULL DEFAULT FALSE
|
||||
COMMENT '是否为该任务类型的默认模板' AFTER content;
|
||||
|
||||
-- 步骤2: 修改 is_active 字段(如果存在且类型不是 BOOLEAN)
|
||||
-- 先检查字段是否存在,如果不存在则添加
|
||||
ALTER TABLE prompts
|
||||
MODIFY COLUMN is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
COMMENT '是否启用(只有启用的提示词才能被使用)';
|
||||
|
||||
-- 步骤3: 删除 tags 字段
|
||||
ALTER TABLE prompts DROP COLUMN IF EXISTS tags;
|
||||
|
||||
-- 步骤4: 从 prompt_config 迁移数据(如果 prompt_config 表存在)
|
||||
-- 更新 task_type 和 is_default
|
||||
UPDATE prompts p
|
||||
LEFT JOIN prompt_config pc ON p.id = pc.prompt_id
|
||||
SET
|
||||
p.task_type = CASE
|
||||
WHEN pc.task_name IS NOT NULL THEN pc.task_name
|
||||
ELSE 'MEETING_TASK' -- 默认值
|
||||
END,
|
||||
p.is_default = CASE
|
||||
WHEN pc.is_default = 1 THEN TRUE
|
||||
ELSE FALSE
|
||||
END
|
||||
WHERE pc.prompt_id IS NOT NULL OR p.task_type IS NULL;
|
||||
|
||||
-- 步骤5: 为所有没有设置 task_type 的提示词设置默认值
|
||||
UPDATE prompts
|
||||
SET task_type = 'MEETING_TASK'
|
||||
WHERE task_type IS NULL;
|
||||
|
||||
-- 步骤6: 将 task_type 设置为 NOT NULL
|
||||
ALTER TABLE prompts
|
||||
MODIFY COLUMN task_type ENUM('MEETING_TASK', 'KNOWLEDGE_TASK') NOT NULL
|
||||
COMMENT '任务类型:MEETING_TASK-会议任务, KNOWLEDGE_TASK-知识库任务';
|
||||
|
||||
-- 步骤7: 确保每个 task_type 只有一个默认提示词
|
||||
-- 如果有多个默认,只保留 id 最小的那个
|
||||
UPDATE prompts p1
|
||||
LEFT JOIN (
|
||||
SELECT task_type, MIN(id) as min_id
|
||||
FROM prompts
|
||||
WHERE is_default = TRUE
|
||||
GROUP BY task_type
|
||||
) p2 ON p1.task_type = p2.task_type
|
||||
SET p1.is_default = FALSE
|
||||
WHERE p1.is_default = TRUE AND p1.id != p2.min_id;
|
||||
|
||||
-- 步骤8: (可选) 备注 prompt_config 表已废弃
|
||||
-- 如果需要删除 prompt_config 表,取消下面的注释
|
||||
-- DROP TABLE IF EXISTS prompt_config;
|
||||
|
||||
-- 迁移完成
|
||||
SELECT '提示词表迁移完成!' as message;
|
||||
SELECT task_type, COUNT(*) as total, SUM(is_default) as default_count
|
||||
FROM prompts
|
||||
GROUP BY task_type;
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
-- 为terminals表添加当前绑定用户ID字段
|
||||
ALTER TABLE `terminals`
|
||||
ADD COLUMN `current_user_id` INT DEFAULT NULL COMMENT '当前绑定/使用的用户ID',
|
||||
ADD CONSTRAINT `fk_terminals_current_user` FOREIGN KEY (`current_user_id`) REFERENCES `users` (`user_id`) ON DELETE SET NULL;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
-- ============================================
|
||||
-- 为 audio_files 表添加 duration 字段
|
||||
-- 创建时间: 2025-01-26
|
||||
-- 说明: 添加音频时长字段(秒),用于统计用户会议总时长
|
||||
-- ============================================
|
||||
|
||||
-- 添加 duration 字段(单位:秒)
|
||||
ALTER TABLE audio_files
|
||||
ADD COLUMN duration INT(11) DEFAULT 0 COMMENT '音频时长(秒)'
|
||||
AFTER file_size;
|
||||
|
||||
-- 添加索引以提高查询性能
|
||||
ALTER TABLE audio_files
|
||||
ADD INDEX idx_duration (duration);
|
||||
|
||||
-- ============================================
|
||||
-- 验证修改
|
||||
-- ============================================
|
||||
-- 查看表结构
|
||||
-- DESCRIBE audio_files;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
ALTER TABLE `audio_model_config`
|
||||
ADD COLUMN `extra_config` JSON DEFAULT NULL COMMENT '音频模型差异化配置(JSON)' AFTER `hot_word_group_id`;
|
||||
|
||||
UPDATE `audio_model_config`
|
||||
SET `extra_config` = CASE
|
||||
WHEN `audio_scene` = 'asr' THEN JSON_OBJECT(
|
||||
'model', `asr_model_name`,
|
||||
'vocabulary_id', `asr_vocabulary_id`,
|
||||
'speaker_count', `asr_speaker_count`,
|
||||
'language_hints', `asr_language_hints`,
|
||||
'disfluency_removal_enabled', `asr_disfluency_removal_enabled`,
|
||||
'diarization_enabled', `asr_diarization_enabled`
|
||||
)
|
||||
WHEN `audio_scene` = 'voiceprint' THEN JSON_OBJECT(
|
||||
'model', `model_name`,
|
||||
'template_text', `vp_template_text`,
|
||||
'duration_seconds', `vp_duration_seconds`,
|
||||
'sample_rate', `vp_sample_rate`,
|
||||
'channels', `vp_channels`,
|
||||
'max_size_bytes', `vp_max_size_bytes`
|
||||
)
|
||||
ELSE JSON_OBJECT()
|
||||
END
|
||||
WHERE `extra_config` IS NULL;
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
START TRANSACTION;
|
||||
|
||||
SET @meeting_manage_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'meeting_manage' LIMIT 1);
|
||||
SET @history_meetings_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'history_meetings' LIMIT 1);
|
||||
SET @prompt_config_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'prompt_config' LIMIT 1);
|
||||
SET @personal_prompt_library_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'personal_prompt_library' LIMIT 1);
|
||||
|
||||
INSERT INTO sys_menus (
|
||||
menu_code, menu_name, menu_icon, menu_url, menu_type, parent_id, sort_order, is_active, description
|
||||
)
|
||||
SELECT
|
||||
'history_meetings', '历史会议', 'CalendarOutlined', '/meetings/history', 'link', @meeting_manage_id, 1, 1, '普通用户历史会议'
|
||||
FROM DUAL
|
||||
WHERE @meeting_manage_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM sys_menus WHERE menu_code = 'history_meetings');
|
||||
|
||||
SET @history_meetings_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'history_meetings' LIMIT 1);
|
||||
|
||||
UPDATE sys_menus
|
||||
SET
|
||||
parent_id = @meeting_manage_id,
|
||||
sort_order = 2,
|
||||
menu_name = '提示词配置',
|
||||
menu_icon = COALESCE(menu_icon, 'BookOutlined')
|
||||
WHERE menu_code = 'prompt_config';
|
||||
|
||||
INSERT INTO sys_menus (
|
||||
menu_code, menu_name, menu_icon, menu_url, menu_type, parent_id, sort_order, is_active, description
|
||||
)
|
||||
SELECT
|
||||
'personal_prompt_library', '个人提示词仓库', 'ReadOutlined', '/personal-prompts', 'link', @meeting_manage_id, 3, 1, '普通用户个人提示词仓库'
|
||||
FROM DUAL
|
||||
WHERE @meeting_manage_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM sys_menus WHERE menu_code = 'personal_prompt_library');
|
||||
|
||||
SET @personal_prompt_library_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'personal_prompt_library' LIMIT 1);
|
||||
|
||||
INSERT IGNORE INTO sys_role_menu_permissions (role_id, menu_id, granted_at)
|
||||
SELECT 2, @meeting_manage_id, NOW()
|
||||
FROM DUAL
|
||||
WHERE @meeting_manage_id IS NOT NULL;
|
||||
|
||||
INSERT IGNORE INTO sys_role_menu_permissions (role_id, menu_id, granted_at)
|
||||
SELECT 2, @history_meetings_id, NOW()
|
||||
FROM DUAL
|
||||
WHERE @history_meetings_id IS NOT NULL;
|
||||
|
||||
INSERT IGNORE INTO sys_role_menu_permissions (role_id, menu_id, granted_at)
|
||||
SELECT 2, @prompt_config_id, NOW()
|
||||
FROM DUAL
|
||||
WHERE @prompt_config_id IS NOT NULL;
|
||||
|
||||
INSERT IGNORE INTO sys_role_menu_permissions (role_id, menu_id, granted_at)
|
||||
SELECT 2, @personal_prompt_library_id, NOW()
|
||||
FROM DUAL
|
||||
WHERE @personal_prompt_library_id IS NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
-- ============================================
|
||||
-- 添加 prompt_id 字段到主表
|
||||
-- 创建时间: 2025-01-11
|
||||
-- 说明: 在 meetings 和 knowledge_bases 表中添加 prompt_id 字段
|
||||
-- 用于记录会议/知识库使用的提示词模版
|
||||
-- ============================================
|
||||
|
||||
-- 1. 为 meetings 表添加 prompt_id 字段
|
||||
ALTER TABLE meetings
|
||||
ADD COLUMN prompt_id INT(11) DEFAULT 0 COMMENT '使用的提示词模版ID,0表示未使用或使用默认模版'
|
||||
AFTER summary;
|
||||
|
||||
-- 为 meetings 表添加索引
|
||||
ALTER TABLE meetings
|
||||
ADD INDEX idx_prompt_id (prompt_id);
|
||||
|
||||
-- 2. 为 knowledge_bases 表添加 prompt_id 字段
|
||||
ALTER TABLE knowledge_bases
|
||||
ADD COLUMN prompt_id INT(11) DEFAULT 0 COMMENT '使用的提示词模版ID,0表示未使用或使用默认模版'
|
||||
AFTER tags;
|
||||
|
||||
-- 为 knowledge_bases 表添加索引
|
||||
ALTER TABLE knowledge_bases
|
||||
ADD INDEX idx_prompt_id (prompt_id);
|
||||
|
||||
-- ============================================
|
||||
-- 验证修改
|
||||
-- ============================================
|
||||
-- 查看 meetings 表结构
|
||||
-- DESCRIBE meetings;
|
||||
|
||||
-- 查看 knowledge_bases 表结构
|
||||
-- DESCRIBE knowledge_bases;
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
SET @request_timeout_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'audio_model_config'
|
||||
AND COLUMN_NAME = 'request_timeout_seconds'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@request_timeout_exists = 0,
|
||||
'ALTER TABLE `audio_model_config` ADD COLUMN `request_timeout_seconds` int(11) NOT NULL DEFAULT 300 COMMENT ''音频转录请求超时(秒)'' AFTER `api_key`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @has_asr_legacy := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'audio_model_config'
|
||||
AND COLUMN_NAME = 'asr_model_name'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@has_asr_legacy > 0,
|
||||
'UPDATE `audio_model_config`
|
||||
SET `extra_config` = JSON_SET(
|
||||
COALESCE(`extra_config`, JSON_OBJECT()),
|
||||
''$.model'', `asr_model_name`,
|
||||
''$.vocabulary_id'', `asr_vocabulary_id`,
|
||||
''$.speaker_count'', `asr_speaker_count`,
|
||||
''$.language_hints'', `asr_language_hints`,
|
||||
''$.disfluency_removal_enabled'', `asr_disfluency_removal_enabled`,
|
||||
''$.diarization_enabled'', `asr_diarization_enabled`
|
||||
)
|
||||
WHERE `audio_scene` = ''asr''',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @has_voiceprint_legacy := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'audio_model_config'
|
||||
AND COLUMN_NAME = 'vp_template_text'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@has_voiceprint_legacy > 0,
|
||||
'UPDATE `audio_model_config`
|
||||
SET `extra_config` = JSON_SET(
|
||||
COALESCE(`extra_config`, JSON_OBJECT()),
|
||||
''$.template_text'', `vp_template_text`,
|
||||
''$.duration_seconds'', `vp_duration_seconds`,
|
||||
''$.sample_rate'', `vp_sample_rate`,
|
||||
''$.channels'', `vp_channels`,
|
||||
''$.max_size_bytes'', `vp_max_size_bytes`
|
||||
)
|
||||
WHERE `audio_scene` = ''voiceprint''',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
UPDATE `audio_model_config`
|
||||
SET `request_timeout_seconds` = COALESCE(
|
||||
NULLIF(`request_timeout_seconds`, 0),
|
||||
CAST(JSON_UNQUOTE(JSON_EXTRACT(`extra_config`, '$.request_timeout_seconds')) AS UNSIGNED),
|
||||
300
|
||||
);
|
||||
|
||||
SET SESSION group_concat_max_len = 8192;
|
||||
SELECT GROUP_CONCAT(CONCAT('DROP COLUMN `', COLUMN_NAME, '`') ORDER BY ORDINAL_POSITION SEPARATOR ', ')
|
||||
INTO @drop_columns_sql
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'audio_model_config'
|
||||
AND COLUMN_NAME IN (
|
||||
'asr_model_name',
|
||||
'asr_vocabulary_id',
|
||||
'asr_speaker_count',
|
||||
'asr_language_hints',
|
||||
'asr_disfluency_removal_enabled',
|
||||
'asr_diarization_enabled',
|
||||
'vp_template_text',
|
||||
'vp_duration_seconds',
|
||||
'vp_sample_rate',
|
||||
'vp_channels',
|
||||
'vp_max_size_bytes'
|
||||
);
|
||||
|
||||
SET @sql := IF(
|
||||
@drop_columns_sql IS NULL OR @drop_columns_sql = '',
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `audio_model_config` ', @drop_columns_sql)
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
DROP TABLE IF EXISTS `ai_model_configs`;
|
||||
DROP TABLE IF EXISTS `ai_model_config`;
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
-- Migration: create parameter/model management and migrate system_config
|
||||
-- Created at: 2026-03-12
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sys_system_parameters` (
|
||||
`param_id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`param_key` varchar(128) NOT NULL,
|
||||
`param_name` varchar(255) NOT NULL,
|
||||
`param_value` text,
|
||||
`value_type` varchar(32) NOT NULL DEFAULT 'string',
|
||||
`category` varchar(64) NOT NULL DEFAULT 'system',
|
||||
`description` varchar(500) DEFAULT NULL,
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`param_id`),
|
||||
UNIQUE KEY `uk_param_key` (`param_key`),
|
||||
KEY `idx_param_category` (`category`),
|
||||
KEY `idx_param_active` (`is_active`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ai_model_configs` (
|
||||
`model_id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`model_code` varchar(128) NOT NULL,
|
||||
`model_name` varchar(255) NOT NULL,
|
||||
`model_type` varchar(32) NOT NULL,
|
||||
`provider` varchar(64) DEFAULT NULL,
|
||||
`config_json` json DEFAULT NULL,
|
||||
`description` varchar(500) DEFAULT NULL,
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`is_default` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`model_id`),
|
||||
UNIQUE KEY `uk_model_code` (`model_code`),
|
||||
KEY `idx_model_type` (`model_type`),
|
||||
KEY `idx_model_active` (`is_active`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- migrate system_config parameters except model-like records
|
||||
INSERT INTO `sys_system_parameters` (`param_key`, `param_name`, `param_value`, `value_type`, `category`, `description`, `is_active`)
|
||||
SELECT
|
||||
CASE
|
||||
WHEN d.`dict_code` = 'timeline_pagesize' THEN 'page_size'
|
||||
WHEN d.`dict_code` = 'branding_app_name' THEN 'app_name'
|
||||
WHEN d.`dict_code` = 'branding_console_subtitle' THEN 'console_subtitle'
|
||||
WHEN d.`dict_code` = 'branding_preview_title' THEN 'preview_title'
|
||||
WHEN d.`dict_code` = 'branding_login_welcome' THEN 'login_welcome'
|
||||
WHEN d.`dict_code` = 'branding_footer_text' THEN 'footer_text'
|
||||
ELSE d.`dict_code`
|
||||
END,
|
||||
d.`label_cn`,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(d.`extension_attr`, '$.value')),
|
||||
'string',
|
||||
CASE
|
||||
WHEN d.`dict_code` IN (
|
||||
'timeline_pagesize',
|
||||
'page_size',
|
||||
'branding_app_name',
|
||||
'app_name',
|
||||
'branding_console_subtitle',
|
||||
'console_subtitle',
|
||||
'branding_preview_title',
|
||||
'preview_title',
|
||||
'branding_login_welcome',
|
||||
'login_welcome',
|
||||
'branding_footer_text',
|
||||
'footer_text',
|
||||
'max_audio_size',
|
||||
'max_image_size'
|
||||
) THEN 'public'
|
||||
ELSE 'system'
|
||||
END,
|
||||
CONCAT('migrated from dict_data.system_config(', d.`dict_code`, ')'),
|
||||
CASE WHEN d.`status` = 1 THEN 1 ELSE 0 END
|
||||
FROM `sys_dict_data` d
|
||||
WHERE d.`dict_type` = 'system_config'
|
||||
AND d.`dict_code` NOT IN ('llm_model', 'voiceprint')
|
||||
AND JSON_EXTRACT(d.`extension_attr`, '$.value') IS NOT NULL
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`param_name` = VALUES(`param_name`),
|
||||
`param_value` = VALUES(`param_value`),
|
||||
`category` = VALUES(`category`),
|
||||
`is_active` = VALUES(`is_active`);
|
||||
|
||||
-- migrate llm model
|
||||
INSERT INTO `ai_model_configs` (`model_code`, `model_name`, `model_type`, `provider`, `config_json`, `description`, `is_active`, `is_default`)
|
||||
SELECT
|
||||
'llm_model',
|
||||
'LLM文本模型',
|
||||
'llm',
|
||||
'dashscope',
|
||||
d.`extension_attr`,
|
||||
'migrated from dict_data.system_config.llm_model',
|
||||
CASE WHEN d.`status` = 1 THEN 1 ELSE 0 END,
|
||||
1
|
||||
FROM `sys_dict_data` d
|
||||
WHERE d.`dict_type` = 'system_config'
|
||||
AND d.`dict_code` = 'llm_model'
|
||||
LIMIT 1
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`config_json` = VALUES(`config_json`),
|
||||
`is_active` = VALUES(`is_active`);
|
||||
|
||||
-- migrate audio model (voiceprint)
|
||||
INSERT INTO `ai_model_configs` (`model_code`, `model_name`, `model_type`, `provider`, `config_json`, `description`, `is_active`, `is_default`)
|
||||
SELECT
|
||||
'voiceprint_model',
|
||||
'声纹模型',
|
||||
'audio',
|
||||
'funasr',
|
||||
d.`extension_attr`,
|
||||
'migrated from dict_data.system_config.voiceprint',
|
||||
CASE WHEN d.`status` = 1 THEN 1 ELSE 0 END,
|
||||
1
|
||||
FROM `sys_dict_data` d
|
||||
WHERE d.`dict_type` = 'system_config'
|
||||
AND d.`dict_code` = 'voiceprint'
|
||||
LIMIT 1
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`config_json` = VALUES(`config_json`),
|
||||
`is_active` = VALUES(`is_active`);
|
||||
|
||||
-- ensure audio ASR model exists (from current hard-coded settings)
|
||||
INSERT INTO `ai_model_configs` (`model_code`, `model_name`, `model_type`, `provider`, `config_json`, `description`, `is_active`, `is_default`)
|
||||
SELECT
|
||||
'audio_model',
|
||||
'音频识别模型',
|
||||
'audio',
|
||||
'dashscope',
|
||||
JSON_OBJECT(
|
||||
'model', 'paraformer-v2',
|
||||
'language_hints', JSON_ARRAY('zh', 'en'),
|
||||
'disfluency_removal_enabled', TRUE,
|
||||
'diarization_enabled', TRUE,
|
||||
'speaker_count', 10,
|
||||
'vocabulary_id', (
|
||||
SELECT JSON_UNQUOTE(JSON_EXTRACT(extension_attr, '$.value'))
|
||||
FROM sys_dict_data
|
||||
WHERE dict_type = 'system_config' AND dict_code = 'asr_vocabulary_id'
|
||||
LIMIT 1
|
||||
)
|
||||
),
|
||||
'默认音频识别模型',
|
||||
1,
|
||||
1
|
||||
FROM dual
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM ai_model_configs WHERE model_code = 'audio_model'
|
||||
);
|
||||
|
||||
-- add new platform submenus
|
||||
INSERT IGNORE INTO `sys_menus`
|
||||
(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `is_visible`, `description`)
|
||||
SELECT
|
||||
'parameter_management',
|
||||
'参数管理',
|
||||
'Setting',
|
||||
'/admin/management/parameter-management',
|
||||
'link',
|
||||
m.`menu_id`,
|
||||
8,
|
||||
1,
|
||||
1,
|
||||
'系统参数管理'
|
||||
FROM `sys_menus` m
|
||||
WHERE m.`menu_code` = 'platform_admin';
|
||||
|
||||
INSERT IGNORE INTO `sys_menus`
|
||||
(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `is_visible`, `description`)
|
||||
SELECT
|
||||
'model_management',
|
||||
'模型管理',
|
||||
'Appstore',
|
||||
'/admin/management/model-management',
|
||||
'link',
|
||||
m.`menu_id`,
|
||||
9,
|
||||
1,
|
||||
1,
|
||||
'音频/LLM模型配置管理'
|
||||
FROM `sys_menus` m
|
||||
WHERE m.`menu_code` = 'platform_admin';
|
||||
|
||||
-- Keep existing role-menu permissions unchanged.
|
||||
-- New menus are added to sys_menus only; authorization is assigned manually.
|
||||
|
||||
-- backfill menu tree metadata for newly inserted rows
|
||||
UPDATE `sys_menus` c
|
||||
JOIN `sys_menus` p ON c.`parent_id` = p.`menu_id`
|
||||
SET c.`menu_level` = p.`menu_level` + 1,
|
||||
c.`tree_path` = CONCAT(p.`tree_path`, '/', c.`menu_id`)
|
||||
WHERE c.`menu_code` IN ('parameter_management', 'model_management')
|
||||
AND (c.`tree_path` IS NULL OR c.`menu_level` = 1);
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
-- Migration: add system management root menu and regroup selected modules
|
||||
-- Created at: 2026-03-12
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ensure system_management root menu exists
|
||||
INSERT INTO sys_menus
|
||||
(menu_code, menu_name, menu_icon, menu_url, menu_type, parent_id, menu_level, tree_path, sort_order, is_active, is_visible, description)
|
||||
SELECT
|
||||
'system_management',
|
||||
'系统管理',
|
||||
'Setting',
|
||||
'/admin/management/user-management',
|
||||
'link',
|
||||
NULL,
|
||||
1,
|
||||
NULL,
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
'系统基础配置管理(用户、权限、字段、参数)'
|
||||
FROM dual
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menus WHERE menu_code = 'system_management');
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
-- ============================================
|
||||
-- 创建用户日志表 (user_logs)
|
||||
-- 创建时间: 2025-01-26
|
||||
-- 说明: 用于记录用户活动日志,包括登录、登出等操作
|
||||
-- 支持查询用户最后登录时间等统计信息
|
||||
-- ============================================
|
||||
|
||||
-- 创建 user_logs 表
|
||||
CREATE TABLE IF NOT EXISTS user_logs (
|
||||
log_id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '日志ID',
|
||||
user_id INT(11) NOT NULL COMMENT '用户ID',
|
||||
action_type VARCHAR(50) NOT NULL COMMENT '操作类型: login, logout, etc.',
|
||||
ip_address VARCHAR(50) DEFAULT NULL COMMENT '用户IP地址',
|
||||
user_agent TEXT DEFAULT NULL COMMENT '用户代理字符串(浏览器/设备信息)',
|
||||
metadata JSON DEFAULT NULL COMMENT '额外的元数据(JSON格式)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '日志创建时间',
|
||||
|
||||
-- 索引
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_action_type (action_type),
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_user_action (user_id, action_type),
|
||||
|
||||
-- 外键约束
|
||||
CONSTRAINT fk_user_logs_user_id FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户活动日志表';
|
||||
|
||||
-- ============================================
|
||||
-- 验证创建
|
||||
-- ============================================
|
||||
-- 查看表结构
|
||||
-- DESCRIBE user_logs;
|
||||
|
||||
-- 查看索引
|
||||
-- SHOW INDEX FROM user_logs;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS `sys_user_mcp` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`bot_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`bot_secret` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`status` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`last_used_at` datetime DEFAULT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_sys_user_mcp_user_id` (`user_id`),
|
||||
UNIQUE KEY `uk_sys_user_mcp_bot_id` (`bot_id`),
|
||||
KEY `idx_sys_user_mcp_status` (`status`),
|
||||
CONSTRAINT `fk_sys_user_mcp_user` FOREIGN KEY (`user_id`) REFERENCES `sys_users` (`user_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户MCP接入凭证';
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
-- Migration: expand ai_model_configs with structured columns
|
||||
-- Created at: 2026-03-12
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE `ai_model_configs`
|
||||
ADD COLUMN `endpoint_url` varchar(512) DEFAULT NULL COMMENT '模型服务 API 地址' AFTER `provider`,
|
||||
ADD COLUMN `api_key` varchar(512) DEFAULT NULL COMMENT '模型服务 API Key' AFTER `endpoint_url`,
|
||||
ADD COLUMN `llm_model_name` varchar(128) DEFAULT NULL COMMENT 'LLM 模型名称' AFTER `api_key`,
|
||||
ADD COLUMN `llm_timeout` int(11) DEFAULT NULL COMMENT 'LLM 超时(秒)' AFTER `llm_model_name`,
|
||||
ADD COLUMN `llm_temperature` decimal(5,2) DEFAULT NULL COMMENT 'LLM temperature' AFTER `llm_timeout`,
|
||||
ADD COLUMN `llm_top_p` decimal(5,2) DEFAULT NULL COMMENT 'LLM top_p' AFTER `llm_temperature`,
|
||||
ADD COLUMN `llm_max_tokens` int(11) DEFAULT NULL COMMENT 'LLM 最大token' AFTER `llm_top_p`,
|
||||
ADD COLUMN `llm_system_prompt` text DEFAULT NULL COMMENT 'LLM 系统提示词' AFTER `llm_max_tokens`,
|
||||
ADD COLUMN `asr_model_name` varchar(128) DEFAULT NULL COMMENT 'ASR 模型名称' AFTER `llm_system_prompt`,
|
||||
ADD COLUMN `asr_vocabulary_id` varchar(255) DEFAULT NULL COMMENT 'ASR 热词词表ID' AFTER `asr_model_name`,
|
||||
ADD COLUMN `asr_speaker_count` int(11) DEFAULT NULL COMMENT 'ASR 说话人数' AFTER `asr_vocabulary_id`,
|
||||
ADD COLUMN `asr_language_hints` varchar(255) DEFAULT NULL COMMENT 'ASR 语言提示,逗号分隔' AFTER `asr_speaker_count`,
|
||||
ADD COLUMN `asr_disfluency_removal_enabled` tinyint(1) DEFAULT NULL COMMENT 'ASR 去口头语开关' AFTER `asr_language_hints`,
|
||||
ADD COLUMN `asr_diarization_enabled` tinyint(1) DEFAULT NULL COMMENT 'ASR 说话人分离开关' AFTER `asr_disfluency_removal_enabled`;
|
||||
|
||||
-- backfill structured columns from existing config_json
|
||||
UPDATE `ai_model_configs`
|
||||
SET
|
||||
endpoint_url = COALESCE(endpoint_url, JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.endpoint_url'))),
|
||||
api_key = COALESCE(api_key, JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.api_key')))
|
||||
WHERE config_json IS NOT NULL;
|
||||
|
||||
UPDATE `ai_model_configs`
|
||||
SET
|
||||
llm_model_name = COALESCE(llm_model_name, JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.model_name'))),
|
||||
llm_timeout = COALESCE(llm_timeout, CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.time_out')) AS UNSIGNED)),
|
||||
llm_temperature = COALESCE(llm_temperature, CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.temperature')) AS DECIMAL(5,2))),
|
||||
llm_top_p = COALESCE(llm_top_p, CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.top_p')) AS DECIMAL(5,2))),
|
||||
llm_max_tokens = COALESCE(llm_max_tokens, CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.max_tokens')) AS UNSIGNED)),
|
||||
llm_system_prompt = COALESCE(llm_system_prompt, JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.system_prompt')))
|
||||
WHERE model_type = 'llm' AND config_json IS NOT NULL;
|
||||
|
||||
UPDATE `ai_model_configs`
|
||||
SET
|
||||
asr_model_name = COALESCE(asr_model_name, JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.model'))),
|
||||
asr_vocabulary_id = COALESCE(asr_vocabulary_id, JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.vocabulary_id'))),
|
||||
asr_speaker_count = COALESCE(asr_speaker_count, CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.speaker_count')) AS UNSIGNED)),
|
||||
asr_language_hints = COALESCE(
|
||||
asr_language_hints,
|
||||
REPLACE(REPLACE(REPLACE(JSON_EXTRACT(config_json, '$.language_hints'), '\"', ''), '[', ''), ']', '')
|
||||
),
|
||||
asr_disfluency_removal_enabled = COALESCE(asr_disfluency_removal_enabled, CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.disfluency_removal_enabled')) AS UNSIGNED)),
|
||||
asr_diarization_enabled = COALESCE(asr_diarization_enabled, CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.diarization_enabled')) AS UNSIGNED))
|
||||
WHERE model_type = 'audio' AND config_json IS NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
-- Migration: expand sys_menus and sys_role_menu_permissions for menu-tree governance
|
||||
-- Created at: 2026-03-03
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1) Extend sys_menus table
|
||||
ALTER TABLE `sys_menus`
|
||||
ADD COLUMN `menu_level` tinyint(3) NOT NULL DEFAULT 1 COMMENT '菜单层级(根节点为1)' AFTER `parent_id`,
|
||||
ADD COLUMN `tree_path` varchar(255) DEFAULT NULL COMMENT '树路径(如 /3/6)' AFTER `menu_level`,
|
||||
ADD COLUMN `is_visible` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否在侧边菜单显示' AFTER `is_active`;
|
||||
|
||||
ALTER TABLE `sys_menus`
|
||||
ADD KEY `idx_menu_level` (`menu_level`),
|
||||
ADD KEY `idx_tree_path` (`tree_path`),
|
||||
ADD KEY `idx_is_visible` (`is_visible`);
|
||||
|
||||
-- 2) Extend sys_role_menu_permissions table
|
||||
ALTER TABLE `sys_role_menu_permissions`
|
||||
ADD COLUMN `granted_by` int(11) DEFAULT NULL COMMENT '授权操作人ID' AFTER `menu_id`,
|
||||
ADD COLUMN `granted_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '授权时间' AFTER `granted_by`;
|
||||
|
||||
ALTER TABLE `sys_role_menu_permissions`
|
||||
ADD KEY `idx_granted_by` (`granted_by`),
|
||||
ADD KEY `idx_granted_at` (`granted_at`);
|
||||
|
||||
-- 3) Backfill tree metadata (supports current 1~2 level menus)
|
||||
UPDATE `sys_menus`
|
||||
SET `menu_level` = 1,
|
||||
`tree_path` = CONCAT('/', `menu_id`)
|
||||
WHERE `parent_id` IS NULL;
|
||||
|
||||
UPDATE `sys_menus` c
|
||||
JOIN `sys_menus` p ON c.`parent_id` = p.`menu_id`
|
||||
SET c.`menu_level` = p.`menu_level` + 1,
|
||||
c.`tree_path` = CONCAT(p.`tree_path`, '/', c.`menu_id`);
|
||||
|
||||
-- 4) Add sample child menus under existing modules
|
||||
INSERT INTO `sys_menus`
|
||||
(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`)
|
||||
SELECT
|
||||
'permission_menu_tree',
|
||||
'菜单树维护',
|
||||
'AppstoreAdd',
|
||||
'/admin/management/permission-management',
|
||||
'link',
|
||||
m.`menu_id`,
|
||||
3,
|
||||
NULL,
|
||||
20,
|
||||
1,
|
||||
0,
|
||||
'权限管理中的菜单树维护入口(隐藏于侧栏)'
|
||||
FROM `sys_menus` m
|
||||
WHERE m.`menu_code` = 'permission_management'
|
||||
AND NOT EXISTS (SELECT 1 FROM `sys_menus` WHERE `menu_code` = 'permission_menu_tree');
|
||||
|
||||
-- backfill tree_path for newly inserted rows
|
||||
UPDATE `sys_menus` c
|
||||
JOIN `sys_menus` p ON c.`parent_id` = p.`menu_id`
|
||||
SET c.`menu_level` = p.`menu_level` + 1,
|
||||
c.`tree_path` = CONCAT(p.`tree_path`, '/', c.`menu_id`)
|
||||
WHERE c.`tree_path` IS NULL;
|
||||
|
||||
-- 5) Keep existing role-menu permissions unchanged.
|
||||
-- Permission assignment is managed explicitly in the application/admin UI.
|
||||
-- Re-running this migration must not backfill or overwrite production grants.
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
START TRANSACTION;
|
||||
|
||||
UPDATE sys_menus
|
||||
SET is_visible = 1,
|
||||
is_active = 1
|
||||
WHERE menu_code IN ('dashboard', 'desktop');
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
-- 让所有角色都能看到 Dashboard 和 Desktop 菜单
|
||||
-- Dashboard sort_order=1, Desktop sort_order=2
|
||||
START TRANSACTION;
|
||||
|
||||
SET @dashboard_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'dashboard' LIMIT 1);
|
||||
SET @desktop_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'desktop' LIMIT 1);
|
||||
|
||||
-- 确保 sort_order 有序
|
||||
UPDATE sys_menus SET sort_order = 1 WHERE menu_code = 'dashboard';
|
||||
UPDATE sys_menus SET sort_order = 2 WHERE menu_code = 'desktop';
|
||||
|
||||
-- 为 role_id=1 (admin) 补充 desktop 权限
|
||||
INSERT IGNORE INTO sys_role_menu_permissions (role_id, menu_id, granted_at)
|
||||
SELECT 1, @desktop_id, NOW()
|
||||
FROM DUAL
|
||||
WHERE @desktop_id IS NOT NULL;
|
||||
|
||||
-- 为 role_id=2 (普通用户) 补充 dashboard 权限
|
||||
INSERT IGNORE INTO sys_role_menu_permissions (role_id, menu_id, granted_at)
|
||||
SELECT 2, @dashboard_id, NOW()
|
||||
FROM DUAL
|
||||
WHERE @dashboard_id IS NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
START TRANSACTION;
|
||||
|
||||
SET @personal_prompt_library_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'personal_prompt_library' LIMIT 1);
|
||||
|
||||
DELETE FROM sys_role_menu_permissions
|
||||
WHERE menu_id = @personal_prompt_library_id;
|
||||
|
||||
UPDATE sys_menus
|
||||
SET is_active = 0
|
||||
WHERE menu_id = @personal_prompt_library_id;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
START TRANSACTION;
|
||||
|
||||
SET @dashboard_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'dashboard' LIMIT 1);
|
||||
SET @desktop_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'desktop' LIMIT 1);
|
||||
SET @account_settings_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'account_settings' LIMIT 1);
|
||||
SET @logout_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'logout' LIMIT 1);
|
||||
|
||||
UPDATE sys_menus
|
||||
SET
|
||||
menu_code = 'dashboard',
|
||||
menu_name = 'Dashboard',
|
||||
menu_icon = 'DashboardOutlined',
|
||||
menu_url = '/dashboard',
|
||||
menu_type = 'link',
|
||||
parent_id = NULL,
|
||||
sort_order = 1,
|
||||
is_active = 1,
|
||||
description = '管理员桌面'
|
||||
WHERE @dashboard_id IS NULL
|
||||
AND menu_id = @account_settings_id;
|
||||
|
||||
INSERT INTO sys_menus (
|
||||
menu_code, menu_name, menu_icon, menu_url, menu_type, parent_id, sort_order, is_active, description
|
||||
)
|
||||
SELECT
|
||||
'dashboard', 'Dashboard', 'DashboardOutlined', '/dashboard', 'link', NULL, 1, 1, '管理员桌面'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_menus WHERE menu_code = 'dashboard'
|
||||
);
|
||||
|
||||
SET @dashboard_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'dashboard' LIMIT 1);
|
||||
|
||||
UPDATE sys_menus
|
||||
SET
|
||||
menu_code = 'desktop',
|
||||
menu_name = 'Desktop',
|
||||
menu_icon = 'DesktopOutlined',
|
||||
menu_url = '/dashboard',
|
||||
menu_type = 'link',
|
||||
parent_id = NULL,
|
||||
sort_order = 1,
|
||||
is_active = 1,
|
||||
description = '普通用户桌面'
|
||||
WHERE @desktop_id IS NULL
|
||||
AND menu_id = @logout_id;
|
||||
|
||||
INSERT INTO sys_menus (
|
||||
menu_code, menu_name, menu_icon, menu_url, menu_type, parent_id, sort_order, is_active, description
|
||||
)
|
||||
SELECT
|
||||
'desktop', 'Desktop', 'DesktopOutlined', '/dashboard', 'link', NULL, 1, 1, '普通用户桌面'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM sys_menus WHERE menu_code = 'desktop'
|
||||
);
|
||||
|
||||
SET @desktop_id := (SELECT menu_id FROM sys_menus WHERE menu_code = 'desktop' LIMIT 1);
|
||||
|
||||
UPDATE sys_menus
|
||||
SET sort_order = 2
|
||||
WHERE menu_code = 'meeting_manage';
|
||||
|
||||
DELETE rmp
|
||||
FROM sys_role_menu_permissions rmp
|
||||
JOIN sys_menus m ON m.menu_id = rmp.menu_id
|
||||
WHERE m.menu_code IN ('account_settings', 'logout');
|
||||
|
||||
DELETE FROM sys_menus
|
||||
WHERE menu_code IN ('account_settings', 'logout');
|
||||
|
||||
DELETE FROM sys_role_menu_permissions
|
||||
WHERE role_id = 1 AND menu_id = @desktop_id;
|
||||
|
||||
DELETE FROM sys_role_menu_permissions
|
||||
WHERE role_id = 2 AND menu_id = @dashboard_id;
|
||||
|
||||
INSERT IGNORE INTO sys_role_menu_permissions (role_id, menu_id, granted_at)
|
||||
SELECT 1, @dashboard_id, NOW()
|
||||
FROM DUAL
|
||||
WHERE @dashboard_id IS NOT NULL;
|
||||
|
||||
INSERT IGNORE INTO sys_role_menu_permissions (role_id, menu_id, granted_at)
|
||||
SELECT 2, @desktop_id, NOW()
|
||||
FROM DUAL
|
||||
WHERE @desktop_id IS NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
-- 将 llm_tasks.result 统一为 /uploads/... 相对路径
|
||||
-- 仅处理能明确识别出 /uploads/ 前缀的历史绝对路径记录
|
||||
|
||||
UPDATE `llm_tasks`
|
||||
SET `result` = SUBSTRING(`result`, LOCATE('/uploads/', `result`))
|
||||
WHERE `result` IS NOT NULL
|
||||
AND `result` <> ''
|
||||
AND `result` NOT LIKE '/uploads/%'
|
||||
AND LOCATE('/uploads/', `result`) > 0;
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
-- Migration: optimize meeting loading performance
|
||||
-- Created at: 2026-04-03
|
||||
|
||||
BEGIN;
|
||||
|
||||
SET @sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'audio_files'
|
||||
AND index_name = 'idx_audio_files_meeting_id'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `audio_files` ADD KEY `idx_audio_files_meeting_id` (`meeting_id`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'transcript_segments'
|
||||
AND index_name = 'idx_transcript_segments_meeting_time'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `transcript_segments` ADD KEY `idx_transcript_segments_meeting_time` (`meeting_id`, `start_time_ms`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'transcript_tasks'
|
||||
AND index_name = 'idx_transcript_tasks_meeting_created'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `transcript_tasks` ADD KEY `idx_transcript_tasks_meeting_created` (`meeting_id`, `created_at`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'llm_tasks'
|
||||
AND index_name = 'idx_llm_tasks_meeting_created'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `llm_tasks` ADD KEY `idx_llm_tasks_meeting_created` (`meeting_id`, `created_at`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'meetings'
|
||||
AND index_name = 'idx_meetings_user_time_created'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE `meetings` ADD KEY `idx_meetings_user_time_created` (`user_id`, `meeting_time`, `created_at`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
-- Migration: optimize menu loading performance
|
||||
-- Created at: 2026-03-12
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1) remove duplicate role-menu mapping rows to allow unique key
|
||||
DELETE r1
|
||||
FROM role_menu_permissions r1
|
||||
JOIN role_menu_permissions r2
|
||||
ON r1.role_id = r2.role_id
|
||||
AND r1.menu_id = r2.menu_id
|
||||
AND r1.id > r2.id;
|
||||
|
||||
-- 2) speed up role-menu lookup and prevent duplicate permission rows
|
||||
ALTER TABLE `role_menu_permissions`
|
||||
ADD UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`),
|
||||
ADD KEY `idx_rmp_role` (`role_id`),
|
||||
ADD KEY `idx_rmp_menu` (`menu_id`);
|
||||
|
||||
-- 3) speed up visible active menu ordering by parent/sort
|
||||
ALTER TABLE `menus`
|
||||
ADD KEY `idx_menus_visible_tree` (`is_active`, `is_visible`, `parent_id`, `sort_order`, `menu_id`);
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
-- Migration: optimize user management query performance
|
||||
-- Created at: 2026-03-12
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE `meetings`
|
||||
ADD KEY `idx_meetings_user_id` (`user_id`);
|
||||
|
||||
ALTER TABLE `attendees`
|
||||
ADD KEY `idx_attendees_user_id` (`user_id`);
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
-- 热词管理:单表 → 主从表(热词组 + 热词条目)
|
||||
-- 执行前请备份 hot_words 表
|
||||
|
||||
-- 1. 创建热词组主表
|
||||
CREATE TABLE IF NOT EXISTS `hot_word_group` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '热词组名称',
|
||||
`description` VARCHAR(500) DEFAULT NULL COMMENT '描述',
|
||||
`vocabulary_id` VARCHAR(255) DEFAULT NULL COMMENT '阿里云 DashScope 词表ID',
|
||||
`last_sync_time` DATETIME DEFAULT NULL COMMENT '最后同步时间',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1:启用 0:停用',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='热词组主表';
|
||||
|
||||
-- 2. 创建热词条目从表
|
||||
CREATE TABLE IF NOT EXISTS `hot_word_item` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`group_id` INT NOT NULL COMMENT '热词组ID',
|
||||
`text` VARCHAR(255) NOT NULL COMMENT '热词内容',
|
||||
`weight` INT NOT NULL DEFAULT 4 COMMENT '权重 1-10',
|
||||
`lang` VARCHAR(20) NOT NULL DEFAULT 'zh' COMMENT 'zh/en',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1:启用 0:停用',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_group_id` (`group_id`),
|
||||
UNIQUE KEY `idx_group_text` (`group_id`, `text`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='热词条目从表';
|
||||
|
||||
-- 3. audio_model_config 新增 hot_word_group_id 列
|
||||
ALTER TABLE `audio_model_config`
|
||||
ADD COLUMN `hot_word_group_id` INT DEFAULT NULL COMMENT '关联热词组ID'
|
||||
AFTER `asr_vocabulary_id`;
|
||||
|
||||
-- 4. 数据迁移:将现有 hot_words 数据迁移到默认组
|
||||
INSERT INTO `hot_word_group` (`name`, `description`, `status`)
|
||||
SELECT '默认热词组', '从旧 hot_words 表迁移的热词', 1
|
||||
FROM DUAL
|
||||
WHERE EXISTS (SELECT 1 FROM `hot_words` LIMIT 1);
|
||||
|
||||
-- 将旧表中已有的 vocabulary_id 回填到默认组(如果存在于 sys_system_parameters)
|
||||
UPDATE `hot_word_group` g
|
||||
JOIN (
|
||||
SELECT param_value FROM `sys_system_parameters`
|
||||
WHERE param_key = 'asr_vocabulary_id' AND is_active = 1
|
||||
LIMIT 1
|
||||
) p ON 1=1
|
||||
SET g.vocabulary_id = p.param_value,
|
||||
g.last_sync_time = NOW()
|
||||
WHERE g.name = '默认热词组';
|
||||
|
||||
-- 迁移热词条目
|
||||
INSERT INTO `hot_word_item` (`group_id`, `text`, `weight`, `lang`, `status`, `create_time`, `update_time`)
|
||||
SELECT g.id, hw.text, hw.weight, hw.lang, hw.status, hw.create_time, hw.update_time
|
||||
FROM `hot_words` hw
|
||||
CROSS JOIN `hot_word_group` g
|
||||
WHERE g.name = '默认热词组';
|
||||
|
||||
-- 5. 将已有 ASR 模型配置关联到默认组
|
||||
UPDATE `audio_model_config` a
|
||||
JOIN `hot_word_group` g ON g.name = '默认热词组'
|
||||
SET a.hot_word_group_id = g.id
|
||||
WHERE a.audio_scene = 'asr'
|
||||
AND a.asr_vocabulary_id IS NOT NULL
|
||||
AND a.asr_vocabulary_id != '';
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
START TRANSACTION;
|
||||
|
||||
UPDATE sys_menus
|
||||
SET
|
||||
menu_code = 'meeting_center',
|
||||
menu_name = '会议中心',
|
||||
menu_icon = 'CalendarOutlined',
|
||||
menu_url = '/meetings/center',
|
||||
description = '普通用户会议中心'
|
||||
WHERE menu_code = 'history_meetings';
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
-- Migration: rename model config tables to singular naming
|
||||
-- Target names:
|
||||
-- llm_model_config
|
||||
-- audio_model_config
|
||||
|
||||
SET @rename_llm_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'llm_model_configs'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'llm_model_config'
|
||||
),
|
||||
'RENAME TABLE llm_model_configs TO llm_model_config',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @rename_llm_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @rename_audio_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'audio_model_configs'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'audio_model_config'
|
||||
),
|
||||
'RENAME TABLE audio_model_configs TO audio_model_config',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @rename_audio_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Remove possible redundant audio/voiceprint fields from llm table (idempotent)
|
||||
SET @drop_audio_scene_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'llm_model_config' AND column_name = 'audio_scene'
|
||||
),
|
||||
'ALTER TABLE llm_model_config DROP COLUMN audio_scene',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @drop_audio_scene_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @drop_asr_model_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'llm_model_config' AND column_name = 'asr_model_name'
|
||||
),
|
||||
'ALTER TABLE llm_model_config DROP COLUMN asr_model_name',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @drop_asr_model_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @drop_asr_vocab_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'llm_model_config' AND column_name = 'asr_vocabulary_id'
|
||||
),
|
||||
'ALTER TABLE llm_model_config DROP COLUMN asr_vocabulary_id',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @drop_asr_vocab_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @drop_vp_tpl_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'llm_model_config' AND column_name = 'vp_template_text'
|
||||
),
|
||||
'ALTER TABLE llm_model_config DROP COLUMN vp_template_text',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @drop_vp_tpl_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @drop_vp_duration_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'llm_model_config' AND column_name = 'vp_duration_seconds'
|
||||
),
|
||||
'ALTER TABLE llm_model_config DROP COLUMN vp_duration_seconds',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @drop_vp_duration_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @drop_vp_rate_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'llm_model_config' AND column_name = 'vp_sample_rate'
|
||||
),
|
||||
'ALTER TABLE llm_model_config DROP COLUMN vp_sample_rate',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @drop_vp_rate_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @drop_vp_channels_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'llm_model_config' AND column_name = 'vp_channels'
|
||||
),
|
||||
'ALTER TABLE llm_model_config DROP COLUMN vp_channels',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @drop_vp_channels_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @drop_vp_size_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'llm_model_config' AND column_name = 'vp_max_size_bytes'
|
||||
),
|
||||
'ALTER TABLE llm_model_config DROP COLUMN vp_max_size_bytes',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @drop_vp_size_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Clean potential non-LLM rows in llm table
|
||||
DELETE FROM llm_model_config
|
||||
WHERE model_code IN ('audio_model', 'voiceprint_model');
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
-- Migration: rename user prompt config table and adjust prompt_config menu URL
|
||||
-- Created at: 2026-03-13
|
||||
|
||||
BEGIN;
|
||||
|
||||
SET @rename_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'sys_user_prompt_config'
|
||||
AND table_type = 'BASE TABLE'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'prompt_config'
|
||||
AND table_type = 'BASE TABLE'
|
||||
),
|
||||
'RENAME TABLE sys_user_prompt_config TO prompt_config',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @rename_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
UPDATE sys_menus
|
||||
SET menu_url = '/prompt-config'
|
||||
WHERE menu_code = 'prompt_config';
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
-- Migration: split LLM and audio model configs into dedicated tables
|
||||
-- Created at: 2026-03-12
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `llm_model_config` (
|
||||
`config_id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`model_code` varchar(128) NOT NULL,
|
||||
`model_name` varchar(255) NOT NULL,
|
||||
`provider` varchar(64) DEFAULT NULL,
|
||||
`endpoint_url` varchar(512) DEFAULT NULL,
|
||||
`api_key` varchar(512) DEFAULT NULL,
|
||||
`llm_model_name` varchar(128) NOT NULL,
|
||||
`llm_timeout` int(11) NOT NULL DEFAULT 120,
|
||||
`llm_temperature` decimal(5,2) NOT NULL DEFAULT 0.70,
|
||||
`llm_top_p` decimal(5,2) NOT NULL DEFAULT 0.90,
|
||||
`llm_max_tokens` int(11) NOT NULL DEFAULT 2048,
|
||||
`llm_system_prompt` text DEFAULT NULL,
|
||||
`description` varchar(500) DEFAULT NULL,
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`is_default` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`config_id`),
|
||||
UNIQUE KEY `uk_llm_model_code` (`model_code`),
|
||||
KEY `idx_llm_active` (`is_active`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `audio_model_config` (
|
||||
`config_id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`model_code` varchar(128) NOT NULL,
|
||||
`model_name` varchar(255) NOT NULL,
|
||||
`audio_scene` varchar(32) NOT NULL COMMENT 'asr / voiceprint',
|
||||
`provider` varchar(64) DEFAULT NULL,
|
||||
`endpoint_url` varchar(512) DEFAULT NULL,
|
||||
`api_key` varchar(512) DEFAULT NULL,
|
||||
`asr_model_name` varchar(128) DEFAULT NULL,
|
||||
`asr_vocabulary_id` varchar(255) DEFAULT NULL,
|
||||
`asr_speaker_count` int(11) DEFAULT NULL,
|
||||
`asr_language_hints` varchar(255) DEFAULT NULL,
|
||||
`asr_disfluency_removal_enabled` tinyint(1) DEFAULT NULL,
|
||||
`asr_diarization_enabled` tinyint(1) DEFAULT NULL,
|
||||
`vp_template_text` text DEFAULT NULL,
|
||||
`vp_duration_seconds` int(11) DEFAULT NULL,
|
||||
`vp_sample_rate` int(11) DEFAULT NULL,
|
||||
`vp_channels` int(11) DEFAULT NULL,
|
||||
`vp_max_size_bytes` bigint(20) DEFAULT NULL,
|
||||
`description` varchar(500) DEFAULT NULL,
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`is_default` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`config_id`),
|
||||
UNIQUE KEY `uk_audio_model_code` (`model_code`),
|
||||
KEY `idx_audio_scene` (`audio_scene`),
|
||||
KEY `idx_audio_active` (`is_active`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- migrate llm rows
|
||||
INSERT INTO `llm_model_config`
|
||||
(model_code, model_name, provider, endpoint_url, api_key, llm_model_name, llm_timeout,
|
||||
llm_temperature, llm_top_p, llm_max_tokens, llm_system_prompt, description, is_active, is_default)
|
||||
SELECT
|
||||
model_code,
|
||||
model_name,
|
||||
provider,
|
||||
endpoint_url,
|
||||
api_key,
|
||||
COALESCE(llm_model_name, JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.model_name')), 'qwen-plus'),
|
||||
COALESCE(llm_timeout, CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.time_out')) AS UNSIGNED), 120),
|
||||
COALESCE(llm_temperature, CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.temperature')) AS DECIMAL(5,2)), 0.70),
|
||||
COALESCE(llm_top_p, CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.top_p')) AS DECIMAL(5,2)), 0.90),
|
||||
COALESCE(llm_max_tokens, CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.max_tokens')) AS UNSIGNED), 2048),
|
||||
COALESCE(llm_system_prompt, JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.system_prompt'))),
|
||||
description,
|
||||
is_active,
|
||||
is_default
|
||||
FROM ai_model_configs
|
||||
WHERE model_type = 'llm'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
model_name = VALUES(model_name),
|
||||
provider = VALUES(provider),
|
||||
endpoint_url = VALUES(endpoint_url),
|
||||
api_key = VALUES(api_key),
|
||||
llm_model_name = VALUES(llm_model_name),
|
||||
llm_timeout = VALUES(llm_timeout),
|
||||
llm_temperature = VALUES(llm_temperature),
|
||||
llm_top_p = VALUES(llm_top_p),
|
||||
llm_max_tokens = VALUES(llm_max_tokens),
|
||||
llm_system_prompt = VALUES(llm_system_prompt),
|
||||
description = VALUES(description),
|
||||
is_active = VALUES(is_active),
|
||||
is_default = VALUES(is_default);
|
||||
|
||||
-- migrate audio recognition rows
|
||||
INSERT INTO `audio_model_config`
|
||||
(model_code, model_name, audio_scene, provider, endpoint_url, api_key, asr_model_name, asr_vocabulary_id,
|
||||
asr_speaker_count, asr_language_hints, asr_disfluency_removal_enabled, asr_diarization_enabled,
|
||||
description, is_active, is_default)
|
||||
SELECT
|
||||
model_code,
|
||||
model_name,
|
||||
'asr',
|
||||
provider,
|
||||
endpoint_url,
|
||||
api_key,
|
||||
COALESCE(asr_model_name, JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.model')), 'paraformer-v2'),
|
||||
COALESCE(asr_vocabulary_id, JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.vocabulary_id'))),
|
||||
COALESCE(asr_speaker_count, CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.speaker_count')) AS UNSIGNED), 10),
|
||||
COALESCE(asr_language_hints, REPLACE(REPLACE(REPLACE(JSON_EXTRACT(config_json, '$.language_hints'), '"', ''), '[', ''), ']', ''), 'zh,en'),
|
||||
COALESCE(asr_disfluency_removal_enabled,
|
||||
CASE LOWER(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.disfluency_removal_enabled'))) WHEN 'true' THEN 1 WHEN '1' THEN 1 ELSE 0 END),
|
||||
COALESCE(asr_diarization_enabled,
|
||||
CASE LOWER(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.diarization_enabled'))) WHEN 'true' THEN 1 WHEN '1' THEN 1 ELSE 0 END),
|
||||
description,
|
||||
is_active,
|
||||
is_default
|
||||
FROM ai_model_configs
|
||||
WHERE model_code = 'audio_model'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
model_name = VALUES(model_name),
|
||||
provider = VALUES(provider),
|
||||
endpoint_url = VALUES(endpoint_url),
|
||||
api_key = VALUES(api_key),
|
||||
asr_model_name = VALUES(asr_model_name),
|
||||
asr_vocabulary_id = VALUES(asr_vocabulary_id),
|
||||
asr_speaker_count = VALUES(asr_speaker_count),
|
||||
asr_language_hints = VALUES(asr_language_hints),
|
||||
asr_disfluency_removal_enabled = VALUES(asr_disfluency_removal_enabled),
|
||||
asr_diarization_enabled = VALUES(asr_diarization_enabled),
|
||||
description = VALUES(description),
|
||||
is_active = VALUES(is_active),
|
||||
is_default = VALUES(is_default);
|
||||
|
||||
-- migrate voiceprint rows
|
||||
INSERT INTO `audio_model_config`
|
||||
(model_code, model_name, audio_scene, provider, endpoint_url, api_key, vp_template_text, vp_duration_seconds,
|
||||
vp_sample_rate, vp_channels, vp_max_size_bytes, description, is_active, is_default)
|
||||
SELECT
|
||||
model_code,
|
||||
model_name,
|
||||
'voiceprint',
|
||||
provider,
|
||||
endpoint_url,
|
||||
api_key,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.template_text')),
|
||||
CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.duration_seconds')) AS UNSIGNED),
|
||||
CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.sample_rate')) AS UNSIGNED),
|
||||
CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.channels')) AS UNSIGNED),
|
||||
CAST(JSON_UNQUOTE(JSON_EXTRACT(config_json, '$.voiceprint_max_size')) AS UNSIGNED),
|
||||
description,
|
||||
is_active,
|
||||
is_default
|
||||
FROM ai_model_configs
|
||||
WHERE model_code = 'voiceprint_model'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
model_name = VALUES(model_name),
|
||||
provider = VALUES(provider),
|
||||
endpoint_url = VALUES(endpoint_url),
|
||||
api_key = VALUES(api_key),
|
||||
vp_template_text = VALUES(vp_template_text),
|
||||
vp_duration_seconds = VALUES(vp_duration_seconds),
|
||||
vp_sample_rate = VALUES(vp_sample_rate),
|
||||
vp_channels = VALUES(vp_channels),
|
||||
vp_max_size_bytes = VALUES(vp_max_size_bytes),
|
||||
description = VALUES(description),
|
||||
is_active = VALUES(is_active),
|
||||
is_default = VALUES(is_default);
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
-- Migration: standardize system-level table names with sys_ prefix
|
||||
-- Strategy:
|
||||
-- 1) Rename physical tables to sys_*
|
||||
-- 2) Create compatibility views with legacy names
|
||||
|
||||
SET @rename_users_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'users' AND table_type = 'BASE TABLE'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'sys_users'
|
||||
),
|
||||
'RENAME TABLE users TO sys_users',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @rename_users_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @rename_roles_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'roles' AND table_type = 'BASE TABLE'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'sys_roles'
|
||||
),
|
||||
'RENAME TABLE roles TO sys_roles',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @rename_roles_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @rename_menus_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'menus' AND table_type = 'BASE TABLE'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'sys_menus'
|
||||
),
|
||||
'RENAME TABLE menus TO sys_menus',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @rename_menus_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @rename_rmp_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'role_menu_permissions' AND table_type = 'BASE TABLE'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'sys_role_menu_permissions'
|
||||
),
|
||||
'RENAME TABLE role_menu_permissions TO sys_role_menu_permissions',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @rename_rmp_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @rename_dict_data_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'dict_data' AND table_type = 'BASE TABLE'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'sys_dict_data'
|
||||
),
|
||||
'RENAME TABLE dict_data TO sys_dict_data',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @rename_dict_data_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @rename_sys_param_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'system_parameters' AND table_type = 'BASE TABLE'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'sys_system_parameters'
|
||||
),
|
||||
'RENAME TABLE system_parameters TO sys_system_parameters',
|
||||
'SELECT 1'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @rename_sys_param_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Drop existing legacy-name views if present, then recreate compatibility views.
|
||||
DROP VIEW IF EXISTS users;
|
||||
DROP VIEW IF EXISTS roles;
|
||||
DROP VIEW IF EXISTS menus;
|
||||
DROP VIEW IF EXISTS role_menu_permissions;
|
||||
DROP VIEW IF EXISTS dict_data;
|
||||
DROP VIEW IF EXISTS system_parameters;
|
||||
|
||||
CREATE VIEW users AS SELECT * FROM sys_users;
|
||||
CREATE VIEW roles AS SELECT * FROM sys_roles;
|
||||
CREATE VIEW menus AS SELECT * FROM sys_menus;
|
||||
CREATE VIEW role_menu_permissions AS SELECT * FROM sys_role_menu_permissions;
|
||||
CREATE VIEW dict_data AS SELECT * FROM sys_dict_data;
|
||||
CREATE VIEW system_parameters AS SELECT * FROM sys_system_parameters;
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
-- Migration: Add avatar_url and update menu for Account Settings
|
||||
-- Created at: 2026-01-15
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. Add avatar_url to users table if it doesn't exist
|
||||
-- Note: MySQL 5.7 doesn't support IF NOT EXISTS for columns easily in one line without procedure,
|
||||
-- but for this environment we assume it doesn't exist or ignore error if strictly handled.
|
||||
-- However, creating a safe idempotent script is better.
|
||||
-- Since I can't run complex procedures easily here, I'll just run the ALTER.
|
||||
-- If it fails, it fails (user can ignore if already applied).
|
||||
ALTER TABLE `users` ADD COLUMN `avatar_url` VARCHAR(512) DEFAULT NULL AFTER `email`;
|
||||
|
||||
-- 2. Remove 'change_password' menu
|
||||
DELETE FROM `role_menu_permissions` WHERE `menu_id` IN (SELECT `menu_id` FROM `menus` WHERE `menu_code` = 'change_password');
|
||||
DELETE FROM `menus` WHERE `menu_code` = 'change_password';
|
||||
|
||||
-- 3. Add 'account_settings' menu
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `sort_order`, `is_active`, `description`)
|
||||
VALUES ('account_settings', '账户设置', 'UserCog', '/account-settings', 'link', 1, 1, '管理个人账户信息');
|
||||
|
||||
-- 4. Grant permissions
|
||||
-- Grant to Admin (role_id=1) and User (role_id=2)
|
||||
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`)
|
||||
SELECT 1, menu_id FROM `menus` WHERE `menu_code` = 'account_settings';
|
||||
|
||||
INSERT INTO `role_menu_permissions` (`role_id`, `menu_id`)
|
||||
SELECT 2, menu_id FROM `menus` WHERE `menu_code` = 'account_settings';
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
-- Migration: convert platform admin menu to hierarchical navigation
|
||||
-- Created at: 2026-03-03
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1) Ensure top-level menus exist and are aligned
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `description`)
|
||||
VALUES ('account_settings', '账户设置', 'UserCog', '/account-settings', 'link', NULL, 1, 1, '管理个人账户信息')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`menu_icon` = VALUES(`menu_icon`),
|
||||
`menu_url` = VALUES(`menu_url`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`is_active` = VALUES(`is_active`),
|
||||
`description` = VALUES(`description`);
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `description`)
|
||||
VALUES ('prompt_management', '提示词仓库', 'BookText', '/prompt-management', 'link', NULL, 2, 1, '管理AI提示词模版')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`menu_icon` = VALUES(`menu_icon`),
|
||||
`menu_url` = VALUES(`menu_url`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`is_active` = VALUES(`is_active`),
|
||||
`description` = VALUES(`description`);
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `description`)
|
||||
VALUES ('platform_admin', '平台管理', 'Shield', '/admin/management/user-management', 'link', NULL, 3, 1, '平台管理员后台')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`menu_icon` = VALUES(`menu_icon`),
|
||||
`menu_url` = VALUES(`menu_url`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`is_active` = VALUES(`is_active`),
|
||||
`description` = VALUES(`description`);
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `description`)
|
||||
VALUES ('logout', '退出登录', 'LogOut', NULL, 'action', NULL, 99, 1, '退出当前账号')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`menu_icon` = VALUES(`menu_icon`),
|
||||
`menu_url` = VALUES(`menu_url`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`is_active` = VALUES(`is_active`),
|
||||
`description` = VALUES(`description`);
|
||||
|
||||
-- 2) Ensure children under platform_admin
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `description`)
|
||||
SELECT 'user_management', '用户管理', 'Users', '/admin/management/user-management', 'link', m.menu_id, 1, 1, '账号、角色、密码重置'
|
||||
FROM `menus` m
|
||||
WHERE m.`menu_code` = 'platform_admin'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`menu_icon` = VALUES(`menu_icon`),
|
||||
`menu_url` = VALUES(`menu_url`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`is_active` = VALUES(`is_active`),
|
||||
`description` = VALUES(`description`);
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `description`)
|
||||
SELECT 'permission_management', '权限管理', 'KeyRound', '/admin/management/permission-management', 'link', m.menu_id, 2, 1, '菜单与角色授权矩阵'
|
||||
FROM `menus` m
|
||||
WHERE m.`menu_code` = 'platform_admin'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`menu_icon` = VALUES(`menu_icon`),
|
||||
`menu_url` = VALUES(`menu_url`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`is_active` = VALUES(`is_active`),
|
||||
`description` = VALUES(`description`);
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `description`)
|
||||
SELECT 'dict_management', '字典管理', 'BookMarked', '/admin/management/dict-management', 'link', m.menu_id, 3, 1, '码表、平台类型、扩展属性'
|
||||
FROM `menus` m
|
||||
WHERE m.`menu_code` = 'platform_admin'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`menu_icon` = VALUES(`menu_icon`),
|
||||
`menu_url` = VALUES(`menu_url`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`is_active` = VALUES(`is_active`),
|
||||
`description` = VALUES(`description`);
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `description`)
|
||||
SELECT 'hot_word_management', '热词管理', 'Text', '/admin/management/hot-word-management', 'link', m.menu_id, 4, 1, 'ASR 热词与同步'
|
||||
FROM `menus` m
|
||||
WHERE m.`menu_code` = 'platform_admin'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`menu_icon` = VALUES(`menu_icon`),
|
||||
`menu_url` = VALUES(`menu_url`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`is_active` = VALUES(`is_active`),
|
||||
`description` = VALUES(`description`);
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `description`)
|
||||
SELECT 'client_management', '客户端管理', 'Smartphone', '/admin/management/client-management', 'link', m.menu_id, 5, 1, '版本、下载地址、发布状态'
|
||||
FROM `menus` m
|
||||
WHERE m.`menu_code` = 'platform_admin'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`menu_icon` = VALUES(`menu_icon`),
|
||||
`menu_url` = VALUES(`menu_url`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`is_active` = VALUES(`is_active`),
|
||||
`description` = VALUES(`description`);
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `description`)
|
||||
SELECT 'external_app_management', '外部应用管理', 'AppWindow', '/admin/management/external-app-management', 'link', m.menu_id, 6, 1, '外部系统入口与图标配置'
|
||||
FROM `menus` m
|
||||
WHERE m.`menu_code` = 'platform_admin'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`menu_icon` = VALUES(`menu_icon`),
|
||||
`menu_url` = VALUES(`menu_url`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`is_active` = VALUES(`is_active`),
|
||||
`description` = VALUES(`description`);
|
||||
|
||||
INSERT INTO `menus` (`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `sort_order`, `is_active`, `description`)
|
||||
SELECT 'terminal_management', '终端管理', 'Monitor', '/admin/management/terminal-management', 'link', m.menu_id, 7, 1, '专用设备、激活和绑定状态'
|
||||
FROM `menus` m
|
||||
WHERE m.`menu_code` = 'platform_admin'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`menu_icon` = VALUES(`menu_icon`),
|
||||
`menu_url` = VALUES(`menu_url`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`sort_order` = VALUES(`sort_order`),
|
||||
`is_active` = VALUES(`is_active`),
|
||||
`description` = VALUES(`description`);
|
||||
|
||||
-- 3) Permission alignment
|
||||
DELETE FROM `role_menu_permissions`
|
||||
WHERE `role_id` = 2
|
||||
AND `menu_id` IN (
|
||||
SELECT `menu_id` FROM `menus`
|
||||
WHERE `menu_code` IN (
|
||||
'platform_admin',
|
||||
'user_management',
|
||||
'permission_management',
|
||||
'dict_management',
|
||||
'hot_word_management',
|
||||
'client_management',
|
||||
'external_app_management',
|
||||
'terminal_management'
|
||||
)
|
||||
);
|
||||
|
||||
INSERT IGNORE INTO `role_menu_permissions` (`role_id`, `menu_id`)
|
||||
SELECT 1, `menu_id`
|
||||
FROM `menus`
|
||||
WHERE `is_active` = 1;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
-- Migration to unify LLM config into a single dict entry 'llm_model'
|
||||
-- Using dict_type='system_config' and dict_code='llm_model'
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Insert default LLM configuration
|
||||
INSERT INTO `dict_data` (
|
||||
`dict_type`, `dict_code`, `parent_code`, `label_cn`, `label_en`,
|
||||
`sort_order`, `extension_attr`, `is_default`, `status`
|
||||
) VALUES (
|
||||
'system_config', 'llm_model', 'ROOT', '大模型配置', 'LLM Model Config',
|
||||
0, '{"model_name": "qwen-plus", "timeout": 120, "temperature": 0.7, "top_p": 0.9}', 0, 1
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`label_cn` = VALUES(`label_cn`);
|
||||
-- Note: We avoid overwriting extension_attr on duplicate key to preserve existing settings if any,
|
||||
-- UNLESS we want to force reset. The user said "refer to...", implying structure exists or should be this.
|
||||
-- If I want to ensure the structure exists with keys, I might need to merge.
|
||||
-- For simplicity, if it exists, I assume it's correct or managed by admin UI.
|
||||
-- But since this is a new "unification", likely it doesn't exist or we want to establish defaults.
|
||||
-- Let's update extension_attr if it's NULL, or just leave it.
|
||||
-- Actually, if I am changing the SCHEMA of config (from individual to unified),
|
||||
-- I should probably populate it.
|
||||
-- Since I cannot easily read old values here, I will just ensure the entry exists.
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
-- 更新voiceprint配置
|
||||
-- 将dict_type='system_config'且dict_code='voiceprint_template'的记录改为 dict_type='voiceprint' 且 dict_code='voiceprint'
|
||||
-- 或者如果已经存在voiceprint类型的配置,则更新它
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. 尝试删除旧的voiceprint配置(如果存在)
|
||||
DELETE FROM `dict_data` WHERE `dict_type` = 'system_config' AND `dict_code` = 'voiceprint_template';
|
||||
|
||||
-- 2. 插入或更新新的voiceprint配置
|
||||
INSERT INTO `dict_data` (
|
||||
`dict_type`, `dict_code`, `parent_code`, `label_cn`, `label_en`,
|
||||
`sort_order`, `extension_attr`, `is_default`, `status`
|
||||
) VALUES (
|
||||
'voiceprint', 'voiceprint', 'ROOT', '声纹配置', 'Voiceprint Config',
|
||||
0, '{"channels": 1, "sample_rate": 16000, "template_text": "我正在进行声纹采集,这段语音将用于身份识别和验证。\n\n声纹技术能够准确识别每个人独特的声音特征。", "duration_seconds": 12}', 0, 1
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`extension_attr` = VALUES(`extension_attr`),
|
||||
`label_cn` = VALUES(`label_cn`);
|
||||
|
||||
COMMIT;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,103 +0,0 @@
|
|||
-- Migration: prompt library upgrade + user prompt config + menu regroup
|
||||
-- Created at: 2026-03-13
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1) prompts table: support system prompt library
|
||||
SET @add_is_system_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'prompts' AND column_name = 'is_system'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE prompts ADD COLUMN is_system TINYINT(1) NOT NULL DEFAULT 0 AFTER creator_id'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @add_is_system_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @add_prompts_idx_sql = (
|
||||
SELECT IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'prompts'
|
||||
AND index_name = 'idx_prompts_task_scope_active'
|
||||
),
|
||||
'SELECT 1',
|
||||
'CREATE INDEX idx_prompts_task_scope_active ON prompts (task_type, is_system, creator_id, is_active, is_default)'
|
||||
)
|
||||
);
|
||||
PREPARE stmt FROM @add_prompts_idx_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Existing admin-created prompts become system prompts by default
|
||||
UPDATE prompts
|
||||
SET is_system = 1
|
||||
WHERE creator_id = 1;
|
||||
|
||||
-- 2) user prompt config table
|
||||
CREATE TABLE IF NOT EXISTS prompt_config (
|
||||
config_id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
user_id INT(11) NOT NULL,
|
||||
task_type ENUM('MEETING_TASK','KNOWLEDGE_TASK') NOT NULL,
|
||||
prompt_id INT(11) NOT NULL,
|
||||
is_enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
sort_order INT(11) NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (config_id),
|
||||
UNIQUE KEY uk_user_task_prompt (user_id, task_type, prompt_id),
|
||||
KEY idx_user_task_order (user_id, task_type, sort_order),
|
||||
KEY idx_prompt_id (prompt_id),
|
||||
CONSTRAINT fk_upc_user FOREIGN KEY (user_id) REFERENCES sys_users(user_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_upc_prompt FOREIGN KEY (prompt_id) REFERENCES prompts(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 3) Menu regroup:
|
||||
-- move prompt_management under platform_admin (2nd level)
|
||||
UPDATE sys_menus child
|
||||
JOIN sys_menus parent ON parent.menu_code = 'platform_admin'
|
||||
SET child.parent_id = parent.menu_id,
|
||||
child.sort_order = 8,
|
||||
child.menu_level = 2,
|
||||
child.tree_path = CONCAT(parent.tree_path, '/', child.menu_id)
|
||||
WHERE child.menu_code = 'prompt_management';
|
||||
|
||||
-- add prompt_config entry
|
||||
INSERT INTO sys_menus
|
||||
(
|
||||
menu_code, menu_name, menu_icon, menu_url, menu_type,
|
||||
parent_id, menu_level, tree_path, sort_order, is_active, is_visible, description
|
||||
)
|
||||
SELECT
|
||||
'prompt_config',
|
||||
'提示词配置',
|
||||
'Book',
|
||||
'/prompt-config',
|
||||
'link',
|
||||
p.menu_id,
|
||||
2,
|
||||
NULL,
|
||||
9,
|
||||
1,
|
||||
1,
|
||||
'用户可配置启用提示词与排序'
|
||||
FROM sys_menus p
|
||||
WHERE p.menu_code = 'platform_admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM sys_menus WHERE menu_code = 'prompt_config');
|
||||
|
||||
UPDATE sys_menus c
|
||||
JOIN sys_menus p ON c.parent_id = p.menu_id
|
||||
SET c.menu_level = p.menu_level + 1,
|
||||
c.tree_path = CONCAT(p.tree_path, '/', c.menu_id)
|
||||
WHERE c.menu_code = 'prompt_config';
|
||||
|
||||
-- Keep existing role-menu permissions unchanged.
|
||||
-- This migration only creates/adjusts menu definitions.
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
-- 为现有数据库添加转录任务支持的SQL脚本
|
||||
|
||||
-- 1. 更新audio_files表结构,添加缺失字段
|
||||
ALTER TABLE audio_files
|
||||
ADD COLUMN file_name VARCHAR(255) AFTER meeting_id,
|
||||
ADD COLUMN file_size BIGINT DEFAULT NULL AFTER file_path,
|
||||
ADD COLUMN task_id VARCHAR(255) DEFAULT NULL AFTER upload_time;
|
||||
|
||||
-- 2. 创建转录任务表
|
||||
CREATE TABLE transcript_tasks (
|
||||
task_id VARCHAR(255) PRIMARY KEY,
|
||||
meeting_id INT NOT NULL,
|
||||
status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending',
|
||||
progress INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP NULL,
|
||||
error_message TEXT NULL,
|
||||
|
||||
FOREIGN KEY (meeting_id) REFERENCES meetings(meeting_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 3. 添加索引以优化查询性能
|
||||
-- audio_files 表索引
|
||||
ALTER TABLE audio_files ADD INDEX idx_task_id (task_id);
|
||||
|
||||
-- transcript_tasks 表索引
|
||||
ALTER TABLE transcript_tasks ADD INDEX idx_meeting_id (meeting_id);
|
||||
ALTER TABLE transcript_tasks ADD INDEX idx_status (status);
|
||||
ALTER TABLE transcript_tasks ADD INDEX idx_created_at (created_at);
|
||||
|
||||
-- 4. 更新现有测试数据(如果需要)
|
||||
-- 这些语句是可选的,用于更新现有的测试数据
|
||||
UPDATE audio_files SET file_name = 'test_audio.mp3' WHERE file_name IS NULL;
|
||||
UPDATE audio_files SET file_size = 10485760 WHERE file_size IS NULL; -- 10MB
|
||||
|
||||
SELECT '转录任务表创建完成!' as message;
|
||||
|
||||
|
||||
CREATE TABLE llm_tasks (
|
||||
task_id VARCHAR(100) PRIMARY KEY,
|
||||
llm_task_id VARCHAR(100) DEFAULT NULL,
|
||||
meeting_id INT NOT NULL,
|
||||
user_prompt TEXT,
|
||||
status VARCHAR(50) DEFAULT 'pending',
|
||||
progress INT DEFAULT 0,
|
||||
result TEXT,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP NULL,
|
||||
INDEX idx_meeting_id (meeting_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
)
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,162 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
API安全性测试脚本
|
||||
测试添加JWT验证后,API端点是否正确拒绝未授权访问
|
||||
|
||||
运行方法:
|
||||
cd /Users/jiliu/工作/projects/imeeting/backend
|
||||
source venv/bin/activate
|
||||
python test/test_api_security.py
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
|
||||
BASE_URL = "http://127.0.0.1:8000"
|
||||
PROXIES = {'http': None, 'https': None}
|
||||
|
||||
def test_unauthorized_access():
|
||||
"""测试未授权访问各个API端点"""
|
||||
print("=== API安全性测试 ===")
|
||||
print("测试未授权访问是否被正确拒绝\n")
|
||||
|
||||
# 需要验证的API端点
|
||||
protected_endpoints = [
|
||||
# Users endpoints
|
||||
("GET", "/api/users", "获取所有用户"),
|
||||
("GET", "/api/users/1", "获取用户详情"),
|
||||
|
||||
# Meetings endpoints
|
||||
("GET", "/api/meetings", "获取会议列表"),
|
||||
("GET", "/api/meetings/1", "获取会议详情"),
|
||||
("GET", "/api/meetings/1/transcript", "获取会议转录"),
|
||||
("GET", "/api/meetings/1/edit", "获取会议编辑信息"),
|
||||
("GET", "/api/meetings/1/audio", "获取会议音频"),
|
||||
("POST", "/api/meetings/1/regenerate-summary", "重新生成摘要"),
|
||||
("GET", "/api/meetings/1/summaries", "获取会议摘要"),
|
||||
("GET", "/api/meetings/1/transcription/status", "获取转录状态"),
|
||||
|
||||
# Auth endpoints (需要token的)
|
||||
("GET", "/api/auth/me", "获取用户信息"),
|
||||
("POST", "/api/auth/logout", "登出"),
|
||||
("POST", "/api/auth/logout-all", "登出所有设备"),
|
||||
]
|
||||
|
||||
success_count = 0
|
||||
total_count = len(protected_endpoints)
|
||||
|
||||
for method, endpoint, description in protected_endpoints:
|
||||
try:
|
||||
url = f"{BASE_URL}{endpoint}"
|
||||
|
||||
if method == "GET":
|
||||
response = requests.get(url, proxies=PROXIES, timeout=5)
|
||||
elif method == "POST":
|
||||
response = requests.post(url, proxies=PROXIES, timeout=5)
|
||||
elif method == "PUT":
|
||||
response = requests.put(url, proxies=PROXIES, timeout=5)
|
||||
elif method == "DELETE":
|
||||
response = requests.delete(url, proxies=PROXIES, timeout=5)
|
||||
|
||||
if response.status_code == 401:
|
||||
print(f"✅ {method} {endpoint} - {description}")
|
||||
print(f" 正确返回401 Unauthorized")
|
||||
success_count += 1
|
||||
else:
|
||||
print(f"❌ {method} {endpoint} - {description}")
|
||||
print(f" 错误:返回 {response.status_code},应该返回401")
|
||||
print(f" 响应: {response.text[:100]}...")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ {method} {endpoint} - {description}")
|
||||
print(f" 请求异常: {e}")
|
||||
|
||||
print()
|
||||
|
||||
print(f"=== 测试结果 ===")
|
||||
print(f"通过: {success_count}/{total_count}")
|
||||
print(f"成功率: {success_count/total_count*100:.1f}%")
|
||||
|
||||
if success_count == total_count:
|
||||
print("🎉 所有API端点都正确实施了JWT验证!")
|
||||
else:
|
||||
print("⚠️ 有些API端点未正确实施JWT验证,需要修复")
|
||||
|
||||
return success_count == total_count
|
||||
|
||||
def test_valid_token_access():
|
||||
"""测试有效token的访问"""
|
||||
print("\n=== 测试有效Token访问 ===")
|
||||
|
||||
# 1. 先登录获取token
|
||||
login_data = {"username": "mula", "password": "781126"}
|
||||
try:
|
||||
response = requests.post(f"{BASE_URL}/api/auth/login", json=login_data, proxies=PROXIES)
|
||||
if response.status_code != 200:
|
||||
print("❌ 无法登录获取测试token")
|
||||
print(f"登录响应: {response.status_code} - {response.text}")
|
||||
return False
|
||||
|
||||
user_data = response.json()
|
||||
token = user_data["token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
print(f"✅ 登录成功,获得token")
|
||||
|
||||
# 2. 测试几个主要API端点
|
||||
test_endpoints = [
|
||||
("GET", "/api/auth/me", "获取当前用户信息"),
|
||||
("GET", "/api/users", "获取用户列表"),
|
||||
("GET", "/api/meetings", "获取会议列表"),
|
||||
]
|
||||
|
||||
success_count = 0
|
||||
for method, endpoint, description in test_endpoints:
|
||||
try:
|
||||
url = f"{BASE_URL}{endpoint}"
|
||||
response = requests.get(url, headers=headers, proxies=PROXIES, timeout=5)
|
||||
|
||||
if response.status_code == 200:
|
||||
print(f"✅ {method} {endpoint} - {description}")
|
||||
print(f" 正确返回200 OK")
|
||||
success_count += 1
|
||||
elif response.status_code == 500:
|
||||
print(f"⚠️ {method} {endpoint} - {description}")
|
||||
print(f" 返回500 (可能是数据库连接问题,但JWT验证通过了)")
|
||||
success_count += 1
|
||||
else:
|
||||
print(f"❌ {method} {endpoint} - {description}")
|
||||
print(f" 意外响应: {response.status_code}")
|
||||
print(f" 响应内容: {response.text[:100]}...")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ {method} {endpoint} - {description}")
|
||||
print(f" 请求异常: {e}")
|
||||
|
||||
print(f"\n有效token测试: {success_count}/{len(test_endpoints)} 通过")
|
||||
return success_count == len(test_endpoints)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 测试失败: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("API JWT安全性测试工具")
|
||||
print("=" * 50)
|
||||
|
||||
# 测试未授权访问
|
||||
unauthorized_ok = test_unauthorized_access()
|
||||
|
||||
# 测试授权访问
|
||||
authorized_ok = test_valid_token_access()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
if unauthorized_ok and authorized_ok:
|
||||
print("🎉 JWT验证实施成功!")
|
||||
print("✅ 未授权访问被正确拒绝")
|
||||
print("✅ 有效token可以正常访问")
|
||||
else:
|
||||
print("⚠️ JWT验证实施不完整")
|
||||
if not unauthorized_ok:
|
||||
print("❌ 部分API未正确拒绝未授权访问")
|
||||
if not authorized_ok:
|
||||
print("❌ 有效token访问存在问题")
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
创建一个新的测试任务
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.services.async_meeting_service import AsyncMeetingService
|
||||
|
||||
# 创建服务实例
|
||||
service = AsyncMeetingService()
|
||||
|
||||
# 创建测试任务
|
||||
meeting_id = 38
|
||||
user_prompt = "请重点关注决策事项和待办任务"
|
||||
|
||||
print("创建新任务...")
|
||||
task_id = service.start_summary_generation(meeting_id, user_prompt)
|
||||
print(f"✅ 任务创建成功: {task_id}")
|
||||
|
||||
# 获取任务状态
|
||||
status = service.get_task_status(task_id)
|
||||
print(f"任务状态: {status}")
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>JWT Token 测试工具</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
textarea { width: 100%; height: 100px; margin: 10px 0; }
|
||||
.result { background: #f5f5f5; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
||||
.error { background: #ffebee; color: #c62828; }
|
||||
.success { background: #e8f5e8; color: #2e7d2e; }
|
||||
button { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
||||
button:hover { background: #0056b3; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>JWT Token 验证工具</h1>
|
||||
|
||||
<h3>步骤1: 从浏览器获取Token</h3>
|
||||
<p>1. 登录你的应用</p>
|
||||
<p>2. 打开开发者工具 → Application → Local Storage → 找到 'iMeetingUser'</p>
|
||||
<p>3. 复制其中的 token 值到下面的文本框</p>
|
||||
|
||||
<textarea id="tokenInput" placeholder="在此粘贴JWT token..."></textarea>
|
||||
<button onclick="decodeToken()">解码 JWT Token</button>
|
||||
|
||||
<div id="result"></div>
|
||||
|
||||
<h3>预期的JWT payload应该包含:</h3>
|
||||
<ul>
|
||||
<li><code>user_id</code>: 用户ID</li>
|
||||
<li><code>username</code>: 用户名</li>
|
||||
<li><code>caption</code>: 用户显示名</li>
|
||||
<li><code>exp</code>: 过期时间戳</li>
|
||||
<li><code>type</code>: "access"</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function decodeToken() {
|
||||
const token = document.getElementById('tokenInput').value.trim();
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
if (!token) {
|
||||
resultDiv.innerHTML = '<div class="result error">请输入JWT token</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// JWT 由三部分组成,用 . 分隔
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('无效的JWT格式');
|
||||
}
|
||||
|
||||
// 解码 header
|
||||
const header = JSON.parse(atob(parts[0]));
|
||||
|
||||
// 解码 payload
|
||||
const payload = JSON.parse(atob(parts[1]));
|
||||
|
||||
// 检查是否是我们的JWT
|
||||
const isValidJWT = payload.type === 'access' &&
|
||||
payload.user_id &&
|
||||
payload.username &&
|
||||
payload.exp;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const isExpired = payload.exp < now;
|
||||
|
||||
let resultHTML = '<div class="result ' + (isValidJWT ? 'success' : 'error') + '">';
|
||||
resultHTML += '<h4>JWT 解码结果:</h4>';
|
||||
resultHTML += '<p><strong>Header:</strong></p>';
|
||||
resultHTML += '<pre>' + JSON.stringify(header, null, 2) + '</pre>';
|
||||
resultHTML += '<p><strong>Payload:</strong></p>';
|
||||
resultHTML += '<pre>' + JSON.stringify(payload, null, 2) + '</pre>';
|
||||
|
||||
if (isExpired) {
|
||||
resultHTML += '<p style="color: red;"><strong>⚠️ Token已过期!</strong></p>';
|
||||
} else {
|
||||
const expireDate = new Date(payload.exp * 1000);
|
||||
resultHTML += '<p style="color: green;"><strong>✅ Token有效,过期时间: ' + expireDate.toLocaleString() + '</strong></p>';
|
||||
}
|
||||
|
||||
if (isValidJWT) {
|
||||
resultHTML += '<p style="color: green;"><strong>✅ 这是有效的iMeeting JWT token!</strong></p>';
|
||||
} else {
|
||||
resultHTML += '<p style="color: red;"><strong>❌ 这不是有效的iMeeting JWT token</strong></p>';
|
||||
}
|
||||
|
||||
resultHTML += '</div>';
|
||||
resultDiv.innerHTML = resultHTML;
|
||||
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = '<div class="result error">解码失败: ' + error.message + '</div>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
"""
|
||||
测试知识库提示词模版选择功能
|
||||
"""
|
||||
import sys
|
||||
sys.path.insert(0, 'app')
|
||||
|
||||
from app.services.llm_service import LLMService
|
||||
from app.services.async_knowledge_base_service import AsyncKnowledgeBaseService
|
||||
from app.core.database import get_db_connection
|
||||
|
||||
def test_get_active_knowledge_prompts():
|
||||
"""测试获取启用的知识库提示词列表"""
|
||||
print("\n=== 测试1: 获取启用的知识库提示词列表 ===")
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 获取KNOWLEDGE_TASK类型的启用模版
|
||||
query = """
|
||||
SELECT id, name, is_default
|
||||
FROM prompts
|
||||
WHERE task_type = %s AND is_active = TRUE
|
||||
ORDER BY is_default DESC, created_at DESC
|
||||
"""
|
||||
cursor.execute(query, ('KNOWLEDGE_TASK',))
|
||||
prompts = cursor.fetchall()
|
||||
|
||||
print(f"✓ 找到 {len(prompts)} 个启用的知识库任务模版:")
|
||||
for p in prompts:
|
||||
default_flag = " [默认]" if p['is_default'] else ""
|
||||
print(f" - ID: {p['id']}, 名称: {p['name']}{default_flag}")
|
||||
|
||||
return prompts
|
||||
except Exception as e:
|
||||
print(f"✗ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
def test_get_task_prompt_with_id(prompts):
|
||||
"""测试通过prompt_id获取知识库提示词内容"""
|
||||
print("\n=== 测试2: 通过prompt_id获取知识库提示词内容 ===")
|
||||
|
||||
if not prompts:
|
||||
print("⚠ 没有可用的提示词模版,跳过测试")
|
||||
return
|
||||
|
||||
llm_service = LLMService()
|
||||
|
||||
# 测试获取第一个提示词
|
||||
test_prompt = prompts[0]
|
||||
try:
|
||||
content = llm_service.get_task_prompt('KNOWLEDGE_TASK', prompt_id=test_prompt['id'])
|
||||
print(f"✓ 成功获取提示词 ID={test_prompt['id']}, 名称={test_prompt['name']}")
|
||||
print(f" 内容长度: {len(content)} 字符")
|
||||
print(f" 内容预览: {content[:100]}...")
|
||||
except Exception as e:
|
||||
print(f"✗ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# 测试获取默认提示词(不指定prompt_id)
|
||||
try:
|
||||
default_content = llm_service.get_task_prompt('KNOWLEDGE_TASK')
|
||||
print(f"✓ 成功获取默认提示词")
|
||||
print(f" 内容长度: {len(default_content)} 字符")
|
||||
except Exception as e:
|
||||
print(f"✗ 获取默认提示词失败: {e}")
|
||||
|
||||
def test_async_kb_service_signature():
|
||||
"""测试async_knowledge_base_service的方法签名"""
|
||||
print("\n=== 测试3: 验证方法签名支持prompt_id参数 ===")
|
||||
|
||||
import inspect
|
||||
async_service = AsyncKnowledgeBaseService()
|
||||
|
||||
# 检查start_generation方法签名
|
||||
sig = inspect.signature(async_service.start_generation)
|
||||
params = list(sig.parameters.keys())
|
||||
|
||||
if 'prompt_id' in params:
|
||||
print(f"✓ start_generation 方法支持 prompt_id 参数")
|
||||
print(f" 参数列表: {params}")
|
||||
else:
|
||||
print(f"✗ start_generation 方法缺少 prompt_id 参数")
|
||||
print(f" 参数列表: {params}")
|
||||
|
||||
# 检查_build_prompt方法签名
|
||||
sig2 = inspect.signature(async_service._build_prompt)
|
||||
params2 = list(sig2.parameters.keys())
|
||||
|
||||
if 'prompt_id' in params2:
|
||||
print(f"✓ _build_prompt 方法支持 prompt_id 参数")
|
||||
print(f" 参数列表: {params2}")
|
||||
else:
|
||||
print(f"✗ _build_prompt 方法缺少 prompt_id 参数")
|
||||
print(f" 参数列表: {params2}")
|
||||
|
||||
def test_database_schema():
|
||||
"""测试数据库schema是否包含prompt_id列"""
|
||||
print("\n=== 测试4: 验证数据库schema ===")
|
||||
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 检查knowledge_base_tasks表是否有prompt_id列
|
||||
cursor.execute("""
|
||||
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'knowledge_base_tasks'
|
||||
AND COLUMN_NAME = 'prompt_id'
|
||||
""")
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
print(f"✓ knowledge_base_tasks 表包含 prompt_id 列")
|
||||
print(f" 类型: {result['DATA_TYPE']}")
|
||||
print(f" 可空: {result['IS_NULLABLE']}")
|
||||
print(f" 默认值: {result['COLUMN_DEFAULT']}")
|
||||
else:
|
||||
print(f"✗ knowledge_base_tasks 表缺少 prompt_id 列")
|
||||
except Exception as e:
|
||||
print(f"✗ 数据库检查失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def test_api_model():
|
||||
"""测试API模型定义"""
|
||||
print("\n=== 测试5: 验证API模型定义 ===")
|
||||
|
||||
try:
|
||||
from app.models.models import CreateKnowledgeBaseRequest
|
||||
import inspect
|
||||
|
||||
# 检查CreateKnowledgeBaseRequest模型
|
||||
fields = CreateKnowledgeBaseRequest.model_fields
|
||||
|
||||
if 'prompt_id' in fields:
|
||||
print(f"✓ CreateKnowledgeBaseRequest 包含 prompt_id 字段")
|
||||
print(f" 字段列表: {list(fields.keys())}")
|
||||
else:
|
||||
print(f"✗ CreateKnowledgeBaseRequest 缺少 prompt_id 字段")
|
||||
print(f" 字段列表: {list(fields.keys())}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ API模型检查失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 60)
|
||||
print("开始测试知识库提示词模版选择功能")
|
||||
print("=" * 60)
|
||||
|
||||
# 运行所有测试
|
||||
prompts = test_get_active_knowledge_prompts()
|
||||
test_get_task_prompt_with_id(prompts)
|
||||
test_async_kb_service_signature()
|
||||
test_database_schema()
|
||||
test_api_model()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试完成")
|
||||
print("=" * 60)
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
登录调试脚本 - 诊断JWT认证问题
|
||||
|
||||
运行方法:
|
||||
cd /Users/jiliu/工作/projects/imeeting/backend
|
||||
source venv/bin/activate # 激活虚拟环境
|
||||
python test/test_login_debug.py
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
BASE_URL = "http://127.0.0.1:8000"
|
||||
|
||||
# 禁用代理以避免本地请求被代理
|
||||
PROXIES = {'http': None, 'https': None}
|
||||
|
||||
def test_backend_connection():
|
||||
"""测试后端连接"""
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/", proxies=PROXIES)
|
||||
print(f"✅ 后端服务连接成功: {response.status_code}")
|
||||
return True
|
||||
except requests.exceptions.ConnectionError:
|
||||
print("❌ 无法连接到后端服务")
|
||||
return False
|
||||
|
||||
def test_login_with_debug(username, password):
|
||||
"""详细的登录测试"""
|
||||
print(f"\n=== 测试登录: {username} ===")
|
||||
|
||||
login_data = {
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
|
||||
try:
|
||||
print(f"请求URL: {BASE_URL}/api/auth/login")
|
||||
print(f"请求数据: {json.dumps(login_data, ensure_ascii=False)}")
|
||||
|
||||
response = requests.post(f"{BASE_URL}/api/auth/login", json=login_data, proxies=PROXIES)
|
||||
|
||||
print(f"响应状态码: {response.status_code}")
|
||||
print(f"响应头: {dict(response.headers)}")
|
||||
|
||||
if response.status_code == 200:
|
||||
user_data = response.json()
|
||||
print("✅ 登录成功!")
|
||||
print(f"用户信息: {json.dumps(user_data, ensure_ascii=False, indent=2)}")
|
||||
return user_data.get("token")
|
||||
else:
|
||||
print("❌ 登录失败")
|
||||
print(f"错误内容: {response.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 请求异常: {e}")
|
||||
return None
|
||||
|
||||
def test_authenticated_request(token):
|
||||
"""测试认证请求"""
|
||||
if not token:
|
||||
print("❌ 没有有效token,跳过认证测试")
|
||||
return
|
||||
|
||||
print(f"\n=== 测试认证请求 ===")
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
try:
|
||||
# 测试 /api/auth/me
|
||||
print("测试 /api/auth/me")
|
||||
response = requests.get(f"{BASE_URL}/api/auth/me", headers=headers, proxies=PROXIES)
|
||||
print(f"状态码: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
print("✅ 认证请求成功")
|
||||
print(f"用户信息: {json.dumps(response.json(), ensure_ascii=False, indent=2)}")
|
||||
else:
|
||||
print("❌ 认证请求失败")
|
||||
print(f"错误: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 认证请求异常: {e}")
|
||||
|
||||
def check_database_users():
|
||||
"""检查数据库用户"""
|
||||
try:
|
||||
from app.core.database import get_db_connection
|
||||
|
||||
print(f"\n=== 检查数据库用户 ===")
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_id, username, caption, email FROM users LIMIT 10")
|
||||
users = cursor.fetchall()
|
||||
|
||||
print(f"数据库中的用户 (前10个):")
|
||||
for user in users:
|
||||
print(f" - ID: {user['user_id']}, 用户名: {user['username']}, 名称: {user['caption']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 无法访问数据库: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("JWT登录调试工具")
|
||||
print("=" * 50)
|
||||
|
||||
# 1. 测试后端连接
|
||||
if not test_backend_connection():
|
||||
exit(1)
|
||||
|
||||
# 2. 检查数据库用户
|
||||
check_database_users()
|
||||
|
||||
# 3. 测试登录
|
||||
username = input("\n请输入用户名 (默认: mula): ").strip() or "mula"
|
||||
password = input("请输入密码 (默认: 781126): ").strip() or "781126"
|
||||
|
||||
token = test_login_with_debug(username, password)
|
||||
|
||||
# 4. 测试认证请求
|
||||
test_authenticated_request(token)
|
||||
|
||||
print("\n=== 调试完成 ===")
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试菜单权限数据是否存在
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.core.database import get_db_connection
|
||||
|
||||
def test_menu_permissions():
|
||||
print("=== 测试菜单权限数据 ===\n")
|
||||
|
||||
try:
|
||||
# 连接数据库
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 1. 检查menus表
|
||||
print("1. 检查menus表:")
|
||||
cursor.execute("SELECT COUNT(*) as count FROM menus")
|
||||
menu_count = cursor.fetchone()['count']
|
||||
print(f" - 菜单总数: {menu_count}")
|
||||
|
||||
if menu_count > 0:
|
||||
cursor.execute("SELECT menu_id, menu_code, menu_name, is_active FROM menus ORDER BY sort_order")
|
||||
menus = cursor.fetchall()
|
||||
for menu in menus:
|
||||
print(f" - [{menu['menu_id']}] {menu['menu_name']} ({menu['menu_code']}) - 启用: {menu['is_active']}")
|
||||
else:
|
||||
print(" ⚠️ menus表为空!")
|
||||
|
||||
print()
|
||||
|
||||
# 2. 检查roles表
|
||||
print("2. 检查roles表:")
|
||||
cursor.execute("SELECT * FROM roles ORDER BY role_id")
|
||||
roles = cursor.fetchall()
|
||||
for role in roles:
|
||||
print(f" - [{role['role_id']}] {role['role_name']}")
|
||||
|
||||
print()
|
||||
|
||||
# 3. 检查role_menu_permissions表
|
||||
print("3. 检查role_menu_permissions表:")
|
||||
cursor.execute("SELECT COUNT(*) as count FROM role_menu_permissions")
|
||||
perm_count = cursor.fetchone()['count']
|
||||
print(f" - 权限总数: {perm_count}")
|
||||
|
||||
if perm_count > 0:
|
||||
cursor.execute("""
|
||||
SELECT r.role_name, m.menu_name, rmp.role_id, rmp.menu_id
|
||||
FROM role_menu_permissions rmp
|
||||
JOIN roles r ON rmp.role_id = r.role_id
|
||||
JOIN menus m ON rmp.menu_id = m.menu_id
|
||||
ORDER BY rmp.role_id, m.sort_order
|
||||
""")
|
||||
permissions = cursor.fetchall()
|
||||
|
||||
current_role = None
|
||||
for perm in permissions:
|
||||
if current_role != perm['role_name']:
|
||||
current_role = perm['role_name']
|
||||
print(f"\n {current_role}的权限:")
|
||||
print(f" - {perm['menu_name']}")
|
||||
else:
|
||||
print(" ⚠️ role_menu_permissions表为空!")
|
||||
|
||||
print("\n" + "="*50)
|
||||
|
||||
# 4. 检查是否需要执行SQL脚本
|
||||
if menu_count == 0 or perm_count == 0:
|
||||
print("\n❌ 数据库中缺少菜单或权限数据!")
|
||||
print("请执行以下命令初始化数据:")
|
||||
print("\nmysql -h 10.100.51.161 -u root -psagacity imeeting_dev < backend/sql/add_menu_permissions_system.sql")
|
||||
print("\n或者在MySQL客户端中执行该SQL文件。")
|
||||
else:
|
||||
print("\n✅ 菜单权限数据正常!")
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_menu_permissions()
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
"""
|
||||
测试提示词模版选择功能
|
||||
"""
|
||||
import sys
|
||||
sys.path.insert(0, 'app')
|
||||
|
||||
from app.services.llm_service import LLMService
|
||||
from app.services.async_meeting_service import AsyncMeetingService
|
||||
from app.core.database import get_db_connection
|
||||
|
||||
def test_get_active_prompts():
|
||||
"""测试获取启用的提示词列表"""
|
||||
print("\n=== 测试1: 获取启用的提示词列表 ===")
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 获取MEETING_TASK类型的启用模版
|
||||
query = """
|
||||
SELECT id, name, is_default
|
||||
FROM prompts
|
||||
WHERE task_type = %s AND is_active = TRUE
|
||||
ORDER BY is_default DESC, created_at DESC
|
||||
"""
|
||||
cursor.execute(query, ('MEETING_TASK',))
|
||||
prompts = cursor.fetchall()
|
||||
|
||||
print(f"✓ 找到 {len(prompts)} 个启用的会议任务模版:")
|
||||
for p in prompts:
|
||||
default_flag = " [默认]" if p['is_default'] else ""
|
||||
print(f" - ID: {p['id']}, 名称: {p['name']}{default_flag}")
|
||||
|
||||
return prompts
|
||||
except Exception as e:
|
||||
print(f"✗ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
def test_get_task_prompt_with_id(prompts):
|
||||
"""测试通过prompt_id获取提示词内容"""
|
||||
print("\n=== 测试2: 通过prompt_id获取提示词内容 ===")
|
||||
|
||||
if not prompts:
|
||||
print("⚠ 没有可用的提示词模版,跳过测试")
|
||||
return
|
||||
|
||||
llm_service = LLMService()
|
||||
|
||||
# 测试获取第一个提示词
|
||||
test_prompt = prompts[0]
|
||||
try:
|
||||
content = llm_service.get_task_prompt('MEETING_TASK', prompt_id=test_prompt['id'])
|
||||
print(f"✓ 成功获取提示词 ID={test_prompt['id']}, 名称={test_prompt['name']}")
|
||||
print(f" 内容长度: {len(content)} 字符")
|
||||
print(f" 内容预览: {content[:100]}...")
|
||||
except Exception as e:
|
||||
print(f"✗ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# 测试获取默认提示词(不指定prompt_id)
|
||||
try:
|
||||
default_content = llm_service.get_task_prompt('MEETING_TASK')
|
||||
print(f"✓ 成功获取默认提示词")
|
||||
print(f" 内容长度: {len(default_content)} 字符")
|
||||
except Exception as e:
|
||||
print(f"✗ 获取默认提示词失败: {e}")
|
||||
|
||||
def test_async_meeting_service_signature():
|
||||
"""测试async_meeting_service的方法签名"""
|
||||
print("\n=== 测试3: 验证方法签名支持prompt_id参数 ===")
|
||||
|
||||
import inspect
|
||||
async_service = AsyncMeetingService()
|
||||
|
||||
# 检查start_summary_generation方法签名
|
||||
sig = inspect.signature(async_service.start_summary_generation)
|
||||
params = list(sig.parameters.keys())
|
||||
|
||||
if 'prompt_id' in params:
|
||||
print(f"✓ start_summary_generation 方法支持 prompt_id 参数")
|
||||
print(f" 参数列表: {params}")
|
||||
else:
|
||||
print(f"✗ start_summary_generation 方法缺少 prompt_id 参数")
|
||||
print(f" 参数列表: {params}")
|
||||
|
||||
# 检查monitor_and_auto_summarize方法签名
|
||||
sig2 = inspect.signature(async_service.monitor_and_auto_summarize)
|
||||
params2 = list(sig2.parameters.keys())
|
||||
|
||||
if 'prompt_id' in params2:
|
||||
print(f"✓ monitor_and_auto_summarize 方法支持 prompt_id 参数")
|
||||
print(f" 参数列表: {params2}")
|
||||
else:
|
||||
print(f"✗ monitor_and_auto_summarize 方法缺少 prompt_id 参数")
|
||||
print(f" 参数列表: {params2}")
|
||||
|
||||
def test_database_schema():
|
||||
"""测试数据库schema是否包含prompt_id列"""
|
||||
print("\n=== 测试4: 验证数据库schema ===")
|
||||
|
||||
try:
|
||||
with get_db_connection() as connection:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 检查llm_tasks表是否有prompt_id列
|
||||
cursor.execute("""
|
||||
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'llm_tasks'
|
||||
AND COLUMN_NAME = 'prompt_id'
|
||||
""")
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
print(f"✓ llm_tasks 表包含 prompt_id 列")
|
||||
print(f" 类型: {result['DATA_TYPE']}")
|
||||
print(f" 可空: {result['IS_NULLABLE']}")
|
||||
print(f" 默认值: {result['COLUMN_DEFAULT']}")
|
||||
else:
|
||||
print(f"✗ llm_tasks 表缺少 prompt_id 列")
|
||||
except Exception as e:
|
||||
print(f"✗ 数据库检查失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def test_api_endpoints():
|
||||
"""测试API端点定义"""
|
||||
print("\n=== 测试5: 验证API端点定义 ===")
|
||||
|
||||
try:
|
||||
from app.api.endpoints.meetings import GenerateSummaryRequest
|
||||
import inspect
|
||||
|
||||
# 检查GenerateSummaryRequest模型
|
||||
fields = GenerateSummaryRequest.__fields__
|
||||
|
||||
if 'prompt_id' in fields:
|
||||
print(f"✓ GenerateSummaryRequest 包含 prompt_id 字段")
|
||||
print(f" 字段列表: {list(fields.keys())}")
|
||||
else:
|
||||
print(f"✗ GenerateSummaryRequest 缺少 prompt_id 字段")
|
||||
print(f" 字段列表: {list(fields.keys())}")
|
||||
|
||||
# 检查audio_service.handle_audio_upload签名
|
||||
from app.services.audio_service import handle_audio_upload
|
||||
sig = inspect.signature(handle_audio_upload)
|
||||
params = list(sig.parameters.keys())
|
||||
|
||||
if 'prompt_id' in params:
|
||||
print(f"✓ handle_audio_upload 方法支持 prompt_id 参数")
|
||||
else:
|
||||
print(f"✗ handle_audio_upload 方法缺少 prompt_id 参数")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ API端点检查失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 60)
|
||||
print("开始测试提示词模版选择功能")
|
||||
print("=" * 60)
|
||||
|
||||
# 运行所有测试
|
||||
prompts = test_get_active_prompts()
|
||||
test_get_task_prompt_with_id(prompts)
|
||||
test_async_meeting_service_signature()
|
||||
test_database_schema()
|
||||
test_api_endpoints()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试完成")
|
||||
print("=" * 60)
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
from qiniu import Auth, put_file_v2, BucketManager
|
||||
|
||||
# Add app path
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), 'app'))
|
||||
|
||||
from app.core.config import QINIU_ACCESS_KEY, QINIU_SECRET_KEY, QINIU_BUCKET, QINIU_DOMAIN
|
||||
|
||||
def test_qiniu_connection():
|
||||
print("=== 七牛云连接测试 ===")
|
||||
print(f"Access Key: {QINIU_ACCESS_KEY[:10]}...")
|
||||
print(f"Secret Key: {QINIU_SECRET_KEY[:10]}...")
|
||||
print(f"Bucket: {QINIU_BUCKET}")
|
||||
print(f"Domain: {QINIU_DOMAIN}")
|
||||
|
||||
# 创建认证对象
|
||||
q = Auth(QINIU_ACCESS_KEY, QINIU_SECRET_KEY)
|
||||
|
||||
# 测试1: 生成上传token
|
||||
try:
|
||||
key = "test/connection-test.txt"
|
||||
token = q.upload_token(QINIU_BUCKET, key, 3600)
|
||||
print(f"✓ Token生成成功: {token[:50]}...")
|
||||
except Exception as e:
|
||||
print(f"✗ Token生成失败: {e}")
|
||||
return False
|
||||
|
||||
# 测试2: 列举存储空间 (测试认证是否正确)
|
||||
try:
|
||||
bucket_manager = BucketManager(q)
|
||||
ret, eof, info = bucket_manager.list(QINIU_BUCKET, limit=100)
|
||||
print(f"✓ Bucket访问成功, status_code: {info.status_code}")
|
||||
if ret:
|
||||
print(f" 存储空间中有文件: {len(ret.get('items', []))} 个")
|
||||
except Exception as e:
|
||||
print(f"✗ Bucket访问失败: {e}")
|
||||
return False
|
||||
|
||||
# 测试3: 上传一个小文件
|
||||
test_file = "/Users/jiliu/工作/projects/imeeting/backend/uploads/result.json"
|
||||
if os.path.exists(test_file):
|
||||
try:
|
||||
key = "test/result1.json"
|
||||
token = q.upload_token(QINIU_BUCKET, key, 3600)
|
||||
ret, info = put_file_v2(token, key, test_file, version='v2')
|
||||
|
||||
print(f"上传结果:")
|
||||
print(f" ret: {ret}")
|
||||
print(f" status_code: {info.status_code}")
|
||||
print(f" text_body: {info.text_body}")
|
||||
print(f" url: {info.url}")
|
||||
print(f" req_id: {info.req_id}")
|
||||
print(f" x_log: {info.x_log}")
|
||||
|
||||
if info.status_code == 200:
|
||||
print("✓ 文件上传成功")
|
||||
url = f"http://{QINIU_DOMAIN}/{key}"
|
||||
print(f" 访问URL: {url}")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ 文件上传失败: {info.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 文件上传异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
else:
|
||||
print(f"✗ 测试文件不存在: {test_file}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_qiniu_connection()
|
||||
if success:
|
||||
print("\n🎉 七牛云连接测试成功!")
|
||||
else:
|
||||
print("\n❌ 七牛云连接测试失败!")
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Redis JWT Token 验证脚本
|
||||
用于检查JWT token是否正确存储在Redis中
|
||||
|
||||
运行方法:
|
||||
cd /Users/jiliu/工作/projects/imeeting/backend
|
||||
source venv/bin/activate # 激活虚拟环境
|
||||
python test/test_redis_jwt.py
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import redis
|
||||
import json
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
try:
|
||||
from app.core.config import REDIS_CONFIG
|
||||
print("✅ 成功导入项目配置")
|
||||
except ImportError as e:
|
||||
print(f"❌ 导入项目配置失败: {e}")
|
||||
print("请确保在 backend 目录下运行: python test/test_redis_jwt.py")
|
||||
sys.exit(1)
|
||||
|
||||
def check_jwt_in_redis():
|
||||
"""检查Redis中的JWT token"""
|
||||
try:
|
||||
# 使用项目配置连接Redis
|
||||
r = redis.Redis(**REDIS_CONFIG)
|
||||
|
||||
# 测试连接
|
||||
r.ping()
|
||||
print("✅ Redis连接成功")
|
||||
print(f"连接配置: {REDIS_CONFIG}")
|
||||
|
||||
# 获取所有token相关的keys
|
||||
token_keys = r.keys("token:*")
|
||||
|
||||
if not token_keys:
|
||||
print("❌ Redis中没有找到JWT token")
|
||||
print("提示: 请先通过前端登录以生成token")
|
||||
return False
|
||||
|
||||
print(f"✅ 找到 {len(token_keys)} 个token记录:")
|
||||
|
||||
for key in token_keys:
|
||||
# 解析key格式: token:user_id:jwt_token
|
||||
key_str = key.decode('utf-8') if isinstance(key, bytes) else key
|
||||
parts = key_str.split(":", 2)
|
||||
if len(parts) >= 3:
|
||||
user_id = parts[1]
|
||||
token_preview = parts[2][:20] + "..."
|
||||
ttl = r.ttl(key)
|
||||
value = r.get(key)
|
||||
value_str = value.decode('utf-8') if isinstance(value, bytes) else value
|
||||
|
||||
print(f" - 用户ID: {user_id}")
|
||||
print(f" Token预览: {token_preview}")
|
||||
if ttl > 0:
|
||||
print(f" 剩余时间: {ttl}秒 ({ttl/3600:.1f}小时)")
|
||||
else:
|
||||
print(f" TTL: {ttl} (永不过期)" if ttl == -1 else f" TTL: {ttl} (已过期)")
|
||||
print(f" 状态: {value_str}")
|
||||
print()
|
||||
|
||||
return True
|
||||
|
||||
except redis.ConnectionError:
|
||||
print("❌ 无法连接到Redis服务器")
|
||||
print("请确保Redis服务正在运行:")
|
||||
print(" brew services start redis # macOS")
|
||||
print(" 或 redis-server # 直接启动")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 检查失败: {e}")
|
||||
return False
|
||||
|
||||
def test_token_operations():
|
||||
"""测试token操作"""
|
||||
try:
|
||||
r = redis.Redis(**REDIS_CONFIG)
|
||||
|
||||
print("\n=== Token操作测试 ===")
|
||||
|
||||
# 模拟创建token
|
||||
test_key = "token:999:test_token_12345"
|
||||
r.setex(test_key, 60, "active")
|
||||
print(f"✅ 创建测试token: {test_key}")
|
||||
|
||||
# 检查token存在
|
||||
if r.exists(test_key):
|
||||
print("✅ Token存在性验证通过")
|
||||
|
||||
# 检查TTL
|
||||
ttl = r.ttl(test_key)
|
||||
print(f"✅ Token TTL: {ttl}秒")
|
||||
|
||||
# 删除测试token
|
||||
r.delete(test_key)
|
||||
print("✅ 清理测试token")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Token操作测试失败: {e}")
|
||||
return False
|
||||
|
||||
def test_jwt_service():
|
||||
"""测试JWT服务"""
|
||||
try:
|
||||
from app.services.jwt_service import jwt_service
|
||||
|
||||
print("\n=== JWT服务测试 ===")
|
||||
|
||||
# 测试创建token
|
||||
test_data = {
|
||||
"user_id": 999,
|
||||
"username": "test_user",
|
||||
"caption": "测试用户"
|
||||
}
|
||||
|
||||
token = jwt_service.create_access_token(test_data)
|
||||
print(f"✅ 创建JWT token: {token[:30]}...")
|
||||
|
||||
# 测试验证token
|
||||
payload = jwt_service.verify_token(token)
|
||||
if payload:
|
||||
print(f"✅ Token验证成功: 用户ID={payload['user_id']}, 用户名={payload['username']}")
|
||||
else:
|
||||
print("❌ Token验证失败")
|
||||
return False
|
||||
|
||||
# 测试撤销token
|
||||
revoked = jwt_service.revoke_token(token, test_data["user_id"])
|
||||
print(f"✅ 撤销token: {'成功' if revoked else '失败'}")
|
||||
|
||||
# 验证撤销后token失效
|
||||
payload_after_revoke = jwt_service.verify_token(token)
|
||||
if not payload_after_revoke:
|
||||
print("✅ Token撤销后验证失败,符合预期")
|
||||
else:
|
||||
print("❌ Token撤销后仍然有效,不符合预期")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ JWT服务测试失败: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("JWT + Redis 认证系统测试")
|
||||
print("=" * 50)
|
||||
print(f"工作目录: {os.getcwd()}")
|
||||
print(f"测试脚本路径: {__file__}")
|
||||
|
||||
# 检查Redis中的JWT tokens
|
||||
redis_ok = check_jwt_in_redis()
|
||||
|
||||
# 测试token操作
|
||||
operations_ok = test_token_operations()
|
||||
|
||||
# 测试JWT服务
|
||||
jwt_service_ok = test_jwt_service()
|
||||
|
||||
print("=" * 50)
|
||||
if redis_ok and operations_ok and jwt_service_ok:
|
||||
print("✅ JWT + Redis 认证系统工作正常!")
|
||||
else:
|
||||
print("❌ JWT + Redis 认证系统存在问题")
|
||||
print("\n故障排除建议:")
|
||||
print("1. 确保在 backend 目录下运行测试")
|
||||
print("2. 确保Redis服务正在运行")
|
||||
print("3. 确保已安装所有依赖: pip install -r requirements.txt")
|
||||
print("4. 尝试先通过前端登录生成token")
|
||||
sys.exit(1)
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试Redis连接和LLM任务队列
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import redis
|
||||
from app.core.config import REDIS_CONFIG
|
||||
|
||||
# 连接Redis
|
||||
redis_client = redis.Redis(**REDIS_CONFIG)
|
||||
|
||||
try:
|
||||
# 测试连接
|
||||
redis_client.ping()
|
||||
print("✅ Redis连接成功")
|
||||
|
||||
# 检查任务队列
|
||||
queue_length = redis_client.llen("llm_task_queue")
|
||||
print(f"📋 当前任务队列长度: {queue_length}")
|
||||
|
||||
# 检查所有LLM任务
|
||||
keys = redis_client.keys("llm_task:*")
|
||||
print(f"📊 当前存在的LLM任务: {len(keys)} 个")
|
||||
|
||||
for key in keys:
|
||||
task_data = redis_client.hgetall(key)
|
||||
# key可能是bytes或str
|
||||
if isinstance(key, bytes):
|
||||
task_id = key.decode('utf-8').replace('llm_task:', '')
|
||||
else:
|
||||
task_id = key.replace('llm_task:', '')
|
||||
|
||||
# 获取状态和进度
|
||||
status = task_data.get(b'status', task_data.get('status', 'unknown'))
|
||||
if isinstance(status, bytes):
|
||||
status = status.decode('utf-8')
|
||||
|
||||
progress = task_data.get(b'progress', task_data.get('progress', '0'))
|
||||
if isinstance(progress, bytes):
|
||||
progress = progress.decode('utf-8')
|
||||
|
||||
print(f" - 任务 {task_id[:8]}... 状态: {status}, 进度: {progress}%")
|
||||
|
||||
# 如果任务是pending,重新推送到队列
|
||||
if status == 'pending':
|
||||
print(f" 🔄 发现pending任务,重新推送到队列...")
|
||||
redis_client.lpush("llm_task_queue", task_id)
|
||||
print(f" ✅ 任务 {task_id[:8]}... 已重新推送到队列")
|
||||
|
||||
except redis.ConnectionError as e:
|
||||
print(f"❌ Redis连接失败: {e}")
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试流式LLM服务
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
|
||||
from app.services.llm_service import LLMService
|
||||
|
||||
def test_stream_generation():
|
||||
"""测试流式生成功能"""
|
||||
print("=== 测试流式LLM生成 ===")
|
||||
|
||||
llm_service = LLMService()
|
||||
test_meeting_id = 38 # 使用一个存在的会议ID
|
||||
test_user_prompt = "请重点关注决策事项和待办任务"
|
||||
|
||||
print(f"开始为会议 {test_meeting_id} 生成流式总结...")
|
||||
print("输出内容:")
|
||||
print("-" * 50)
|
||||
|
||||
full_content = ""
|
||||
chunk_count = 0
|
||||
|
||||
try:
|
||||
for chunk in llm_service.generate_meeting_summary_stream(test_meeting_id, test_user_prompt):
|
||||
if chunk.startswith("error:"):
|
||||
print(f"\n生成过程中出现错误: {chunk}")
|
||||
break
|
||||
else:
|
||||
print(chunk, end='', flush=True)
|
||||
full_content += chunk
|
||||
chunk_count += 1
|
||||
|
||||
print(f"\n\n-" * 50)
|
||||
print(f"流式生成完成!")
|
||||
print(f"总共接收到 {chunk_count} 个数据块")
|
||||
print(f"完整内容长度: {len(full_content)} 字符")
|
||||
|
||||
# 测试传统方式(对比)
|
||||
print("\n=== 对比测试传统生成方式 ===")
|
||||
result = llm_service.generate_meeting_summary(test_meeting_id, test_user_prompt)
|
||||
if result.get("error"):
|
||||
print(f"传统方式生成失败: {result['error']}")
|
||||
else:
|
||||
print("传统方式生成成功!")
|
||||
print(f"内容长度: {len(result['content'])} 字符")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n测试过程中出现异常: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_stream_generation()
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
JWT Token 过期测试脚本
|
||||
用于测试JWT token的过期、撤销机制,可以模拟指定用户的token失效
|
||||
|
||||
运行方法:
|
||||
cd /Users/jiliu/工作/projects/imeeting/backend
|
||||
source venv/bin/activate # 激活虚拟环境
|
||||
python test/test_token_expiration.py
|
||||
|
||||
功能:
|
||||
1. 登录指定用户并获取token
|
||||
2. 验证token有效性
|
||||
3. 撤销指定用户的所有token(模拟失效)
|
||||
4. 验证撤销后token失效
|
||||
|
||||
期望结果:在网页上登录的用户执行失效命令后,网页会自动登出
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
BASE_URL = "http://127.0.0.1:8000"
|
||||
|
||||
# 禁用代理以避免本地请求被代理
|
||||
PROXIES = {'http': None, 'https': None}
|
||||
|
||||
def invalidate_user_tokens():
|
||||
"""模拟指定用户的token失效"""
|
||||
print("模拟用户Token失效工具")
|
||||
print("=" * 40)
|
||||
|
||||
# 获取要失效的用户名
|
||||
target_username = input("请输入要失效token的用户名 (默认: mula): ").strip()
|
||||
if not target_username:
|
||||
target_username = "mula"
|
||||
|
||||
# 获取管理员凭据来执行失效操作
|
||||
admin_username = input("请输入管理员用户名 (默认: mula): ").strip()
|
||||
admin_password = input("请输入管理员密码 (默认: 781126): ").strip()
|
||||
|
||||
if not admin_username:
|
||||
admin_username = "mula"
|
||||
if not admin_password:
|
||||
admin_password = "781126"
|
||||
|
||||
try:
|
||||
# 1. 管理员登录获取token
|
||||
print(f"\n步骤1: 管理员登录 ({admin_username})")
|
||||
admin_login_data = {
|
||||
"username": admin_username,
|
||||
"password": admin_password
|
||||
}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/api/auth/login", json=admin_login_data, proxies=PROXIES)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ 管理员登录失败")
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应内容: {response.text}")
|
||||
return
|
||||
|
||||
admin_data = response.json()
|
||||
admin_token = admin_data["token"]
|
||||
admin_headers = {"Authorization": f"Bearer {admin_token}"}
|
||||
|
||||
print(f"✅ 管理员登录成功: {admin_data['username']} ({admin_data['caption']})")
|
||||
|
||||
# 2. 如果目标用户不是管理员,先登录目标用户验证token存在
|
||||
if target_username != admin_username:
|
||||
print(f"\n步骤2: 验证目标用户 ({target_username}) 是否存在")
|
||||
target_password = input(f"请输入 {target_username} 的密码 (用于验证): ").strip()
|
||||
if not target_password:
|
||||
print("❌ 需要提供目标用户的密码来验证")
|
||||
return
|
||||
|
||||
target_login_data = {
|
||||
"username": target_username,
|
||||
"password": target_password
|
||||
}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/api/auth/login", json=target_login_data, proxies=PROXIES)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ 目标用户登录失败,无法验证用户存在")
|
||||
return
|
||||
|
||||
target_data = response.json()
|
||||
print(f"✅ 目标用户验证成功: {target_data['username']} ({target_data['caption']})")
|
||||
target_user_id = target_data['user_id']
|
||||
else:
|
||||
target_user_id = admin_data['user_id']
|
||||
|
||||
# 3. 撤销目标用户的所有token
|
||||
print(f"\n步骤3: 撤销用户 {target_username} (ID: {target_user_id}) 的所有token")
|
||||
|
||||
# 使用管理员权限调用新的admin API
|
||||
response = requests.post(f"{BASE_URL}/api/auth/admin/revoke-user-tokens/{target_user_id}",
|
||||
headers=admin_headers, proxies=PROXIES)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print(f"✅ Token撤销成功: {result.get('message', '已撤销所有token')}")
|
||||
|
||||
# 4. 验证token是否真的失效了
|
||||
print(f"\n步骤4: 验证token失效")
|
||||
if target_username != admin_username:
|
||||
# 尝试使用目标用户的token访问protected API
|
||||
target_token = target_data["token"]
|
||||
target_headers = {"Authorization": f"Bearer {target_token}"}
|
||||
|
||||
response = requests.get(f"{BASE_URL}/api/auth/me", headers=target_headers, proxies=PROXIES)
|
||||
if response.status_code == 401:
|
||||
print(f"✅ 验证成功:用户 {target_username} 的token已失效")
|
||||
else:
|
||||
print(f"❌ 验证失败:用户 {target_username} 的token仍然有效")
|
||||
else:
|
||||
# 如果目标用户就是管理员,验证当前管理员token是否失效
|
||||
response = requests.get(f"{BASE_URL}/api/auth/me", headers=admin_headers, proxies=PROXIES)
|
||||
if response.status_code == 401:
|
||||
print(f"✅ 验证成功:用户 {target_username} 的token已失效")
|
||||
else:
|
||||
print(f"❌ 验证失败:用户 {target_username} 的token仍然有效")
|
||||
|
||||
print(f"\n🌟 操作完成!")
|
||||
print(f"如果用户 {target_username} 在网页上已登录,现在应该会自动登出。")
|
||||
print(f"你可以在网页上验证是否自动跳转到登录页面。")
|
||||
|
||||
else:
|
||||
print(f"❌ Token撤销失败: {response.status_code}")
|
||||
print(f"响应内容: {response.text}")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print("❌ 无法连接到后端服务器,请确保服务器正在运行")
|
||||
except Exception as e:
|
||||
print(f"❌ 操作失败: {e}")
|
||||
|
||||
def test_token_expiration():
|
||||
"""测试token过期机制"""
|
||||
print("JWT Token 过期测试")
|
||||
print("=" * 40)
|
||||
|
||||
# 1. 登录获取token
|
||||
username = input("请输入用户名 (默认: test): ").strip()
|
||||
password = input("请输入密码 (默认: test): ").strip()
|
||||
|
||||
# 使用默认值如果输入为空
|
||||
if not username:
|
||||
username = "test"
|
||||
if not password:
|
||||
password = "test"
|
||||
|
||||
login_data = {
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
|
||||
try:
|
||||
# 登录
|
||||
print(f"正在尝试登录用户: {login_data['username']}")
|
||||
response = requests.post(f"{BASE_URL}/api/auth/login", json=login_data, proxies=PROXIES)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ 登录失败")
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应内容: {response.text}")
|
||||
print(f"请求URL: {BASE_URL}/api/auth/login")
|
||||
print("请检查:")
|
||||
print("1. 后端服务是否正在运行")
|
||||
print("2. 用户名和密码是否正确")
|
||||
print("3. 数据库连接是否正常")
|
||||
return
|
||||
|
||||
user_data = response.json()
|
||||
token = user_data["token"]
|
||||
|
||||
print(f"✅ 登录成功,获得token: {token[:20]}...")
|
||||
|
||||
# 2. 测试token有效性
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
print("\n测试1: 验证token有效性")
|
||||
response = requests.get(f"{BASE_URL}/api/auth/me", headers=headers, proxies=PROXIES)
|
||||
if response.status_code == 200:
|
||||
user_info = response.json()
|
||||
print(f"✅ Token有效,用户: {user_info.get('username')}")
|
||||
else:
|
||||
print(f"❌ Token无效: {response.status_code}")
|
||||
return
|
||||
|
||||
# 3. 测试受保护的API
|
||||
print("\n测试2: 访问受保护的API")
|
||||
response = requests.get(f"{BASE_URL}/api/meetings", headers=headers, proxies=PROXIES)
|
||||
if response.status_code == 200:
|
||||
print("✅ 成功访问会议列表API")
|
||||
else:
|
||||
print(f"❌ 访问受保护API失败: {response.status_code}")
|
||||
|
||||
# 4. 登出token
|
||||
print("\n测试3: 登出token")
|
||||
response = requests.post(f"{BASE_URL}/api/auth/logout", headers=headers, proxies=PROXIES)
|
||||
if response.status_code == 200:
|
||||
print("✅ 登出成功")
|
||||
|
||||
# 5. 验证登出后token失效
|
||||
print("\n测试4: 验证登出后token失效")
|
||||
response = requests.get(f"{BASE_URL}/api/auth/me", headers=headers, proxies=PROXIES)
|
||||
if response.status_code == 401:
|
||||
print("✅ Token已失效,登出成功")
|
||||
else:
|
||||
print(f"❌ Token仍然有效,登出失败: {response.status_code}")
|
||||
else:
|
||||
print(f"❌ 登出失败: {response.status_code}")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print("❌ 无法连接到后端服务器,请确保服务器正在运行")
|
||||
except Exception as e:
|
||||
print(f"❌ 测试失败: {e}")
|
||||
|
||||
def check_token_format():
|
||||
"""检查token格式是否为JWT"""
|
||||
token = input("\n请粘贴JWT token (或按Enter跳过): ").strip()
|
||||
|
||||
if not token:
|
||||
return
|
||||
|
||||
print(f"\nJWT格式检查:")
|
||||
|
||||
# JWT应该有三个部分,用.分隔
|
||||
parts = token.split('.')
|
||||
if len(parts) != 3:
|
||||
print("❌ 不是有效的JWT格式 (应该有3个部分用.分隔)")
|
||||
return
|
||||
|
||||
try:
|
||||
import base64
|
||||
import json
|
||||
|
||||
# 解码header
|
||||
header_padding = parts[0] + '=' * (4 - len(parts[0]) % 4)
|
||||
header = json.loads(base64.urlsafe_b64decode(header_padding))
|
||||
|
||||
# 解码payload
|
||||
payload_padding = parts[1] + '=' * (4 - len(parts[1]) % 4)
|
||||
payload = json.loads(base64.urlsafe_b64decode(payload_padding))
|
||||
|
||||
print("✅ JWT格式有效")
|
||||
print(f"算法: {header.get('alg')}")
|
||||
print(f"类型: {header.get('typ')}")
|
||||
print(f"用户ID: {payload.get('user_id')}")
|
||||
print(f"用户名: {payload.get('username')}")
|
||||
|
||||
if 'exp' in payload:
|
||||
exp_time = datetime.fromtimestamp(payload['exp'])
|
||||
print(f"过期时间: {exp_time}")
|
||||
|
||||
if datetime.now() > exp_time:
|
||||
print("❌ Token已过期")
|
||||
else:
|
||||
print("✅ Token未过期")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ JWT解码失败: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("JWT Token 测试工具")
|
||||
print("=" * 50)
|
||||
print(f"工作目录: {os.getcwd()}")
|
||||
print(f"测试脚本路径: {__file__}")
|
||||
print()
|
||||
|
||||
print("请选择功能:")
|
||||
print("1. 模拟指定用户Token失效 (推荐)")
|
||||
print("2. 完整Token过期测试")
|
||||
print("3. JWT格式检查")
|
||||
|
||||
choice = input("\n请输入选项 (1-3, 默认: 1): ").strip()
|
||||
|
||||
if choice == "2":
|
||||
test_token_expiration()
|
||||
check_token_format()
|
||||
elif choice == "3":
|
||||
check_token_format()
|
||||
else:
|
||||
# 默认选择1
|
||||
invalidate_user_tokens()
|
||||
|
||||
print("\n=== 测试完成 ===")
|
||||
print("如果测试失败,请检查:")
|
||||
print("1. 确保后端服务正在运行: python main.py")
|
||||
print("2. 确保在 backend 目录下运行测试")
|
||||
print("3. 确保Redis服务正在运行")
|
||||
print("4. 如果选择了选项1,请在网页上验证用户是否自动登出")
|
||||
|
|
@ -1,275 +0,0 @@
|
|||
"""
|
||||
测试 upload_audio 接口的 auto_summarize 参数
|
||||
"""
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
|
||||
# 配置
|
||||
BASE_URL = "http://localhost:8000/api"
|
||||
# 请替换为你的有效token
|
||||
AUTH_TOKEN = "your_auth_token_here"
|
||||
# 请替换为你的测试会议ID
|
||||
TEST_MEETING_ID = 1
|
||||
|
||||
# 请求头
|
||||
headers = {
|
||||
"Authorization": f"Bearer {AUTH_TOKEN}"
|
||||
}
|
||||
|
||||
|
||||
def test_upload_audio(auto_summarize=True):
|
||||
"""测试音频上传接口"""
|
||||
print("=" * 60)
|
||||
print(f"测试: upload_audio 接口 (auto_summarize={auto_summarize})")
|
||||
print("=" * 60)
|
||||
|
||||
# 准备测试文件
|
||||
audio_file_path = "test_audio.mp3" # 请替换为实际的音频文件路径
|
||||
|
||||
try:
|
||||
with open(audio_file_path, 'rb') as audio_file:
|
||||
files = {
|
||||
'audio_file': ('test_audio.mp3', audio_file, 'audio/mpeg')
|
||||
}
|
||||
data = {
|
||||
'force_replace': 'false',
|
||||
'auto_summarize': 'true' if auto_summarize else 'false'
|
||||
}
|
||||
|
||||
# 发送请求
|
||||
url = f"{BASE_URL}/meetings/upload-audio"
|
||||
print(f"\n发送请求到: {url}")
|
||||
print(f"参数: auto_summarize={data['auto_summarize']}")
|
||||
response = requests.post(url, headers=headers, files=files, data=data)
|
||||
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应内容:")
|
||||
print(json.dumps(response.json(), indent=2, ensure_ascii=False))
|
||||
|
||||
# 如果上传成功,获取任务ID
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
if response_data.get('code') == '200':
|
||||
task_id = response_data['data'].get('task_id')
|
||||
auto_sum = response_data['data'].get('auto_summarize')
|
||||
print(f"\n✓ 上传成功! 转录任务ID: {task_id}")
|
||||
print(f" 自动总结: {'开启' if auto_sum else '关闭'}")
|
||||
if auto_sum:
|
||||
print(f" 提示: 音频已上传,后台正在自动进行转录和总结")
|
||||
else:
|
||||
print(f" 提示: 音频已上传,正在进行转录(不会自动总结)")
|
||||
print(f"\n 可以通过以下接口查询状态:")
|
||||
print(f" - 转录状态: GET /meetings/{TEST_MEETING_ID}/transcription/status")
|
||||
print(f" - 总结任务: GET /meetings/{TEST_MEETING_ID}/llm-tasks")
|
||||
print(f" - 会议详情: GET /meetings/{TEST_MEETING_ID}")
|
||||
return True
|
||||
elif response_data.get('code') == '300':
|
||||
print("\n⚠ 需要确认替换现有文件")
|
||||
return False
|
||||
else:
|
||||
print(f"\n✗ 上传失败")
|
||||
return False
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"\n✗ 错误: 找不到测试音频文件 {audio_file_path}")
|
||||
print("请创建一个测试音频文件或修改 audio_file_path 变量")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"\n✗ 错误: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_get_transcription_status():
|
||||
"""测试获取转录状态接口"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试: 获取转录状态")
|
||||
print("=" * 60)
|
||||
|
||||
url = f"{BASE_URL}/meetings/{TEST_MEETING_ID}/transcription/status"
|
||||
print(f"\n发送请求到: {url}")
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应内容:")
|
||||
print(json.dumps(response.json(), indent=2, ensure_ascii=False))
|
||||
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
if response_data.get('code') == '200':
|
||||
data = response_data['data']
|
||||
print(f"\n✓ 获取转录状态成功!")
|
||||
print(f" - 任务ID: {data.get('task_id')}")
|
||||
print(f" - 状态: {data.get('status')}")
|
||||
print(f" - 进度: {data.get('progress')}%")
|
||||
return data.get('status'), data.get('progress')
|
||||
else:
|
||||
print(f"\n✗ 获取状态失败")
|
||||
return None, None
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 错误: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
def test_get_llm_tasks():
|
||||
"""测试获取LLM任务列表"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试: 获取LLM任务列表")
|
||||
print("=" * 60)
|
||||
|
||||
url = f"{BASE_URL}/meetings/{TEST_MEETING_ID}/llm-tasks"
|
||||
print(f"\n发送请求到: {url}")
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应内容:")
|
||||
print(json.dumps(response.json(), indent=2, ensure_ascii=False))
|
||||
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
if response_data.get('code') == '200':
|
||||
tasks = response_data['data'].get('tasks', [])
|
||||
print(f"\n✓ 获取LLM任务成功! 共 {len(tasks)} 个任务")
|
||||
if tasks:
|
||||
latest_task = tasks[0]
|
||||
print(f" 最新任务:")
|
||||
print(f" - 任务ID: {latest_task.get('task_id')}")
|
||||
print(f" - 状态: {latest_task.get('status')}")
|
||||
print(f" - 进度: {latest_task.get('progress')}%")
|
||||
return latest_task.get('status'), latest_task.get('progress')
|
||||
return None, None
|
||||
else:
|
||||
print(f"\n✗ 获取任务失败")
|
||||
return None, None
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 错误: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
def monitor_progress():
|
||||
"""持续监控处理进度"""
|
||||
print("\n" + "=" * 60)
|
||||
print("持续监控处理进度 (每10秒查询一次)")
|
||||
print("按 Ctrl+C 停止监控")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
transcription_completed = False
|
||||
summary_completed = False
|
||||
|
||||
while True:
|
||||
print(f"\n[{time.strftime('%H:%M:%S')}] 查询状态...")
|
||||
|
||||
# 查询转录状态
|
||||
trans_status, trans_progress = test_get_transcription_status()
|
||||
|
||||
# 如果转录完成,查询总结状态
|
||||
if trans_status == 'completed' and not transcription_completed:
|
||||
print(f"\n✓ 转录已完成!")
|
||||
transcription_completed = True
|
||||
|
||||
if transcription_completed:
|
||||
summ_status, summ_progress = test_get_llm_tasks()
|
||||
if summ_status == 'completed' and not summary_completed:
|
||||
print(f"\n✓ 总结已完成!")
|
||||
summary_completed = True
|
||||
break
|
||||
elif summ_status == 'failed':
|
||||
print(f"\n✗ 总结失败")
|
||||
break
|
||||
|
||||
# 检查转录是否失败
|
||||
if trans_status == 'failed':
|
||||
print(f"\n✗ 转录失败")
|
||||
break
|
||||
|
||||
# 如果全部完成,退出
|
||||
if transcription_completed and summary_completed:
|
||||
print(f"\n✓ 全部完成!")
|
||||
break
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⚠ 用户中断监控")
|
||||
except Exception as e:
|
||||
print(f"\n✗ 监控出错: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("\n")
|
||||
print("╔" + "═" * 58 + "╗")
|
||||
print("║" + " " * 12 + "upload_audio 接口测试" + " " * 23 + "║")
|
||||
print("║" + " " * 10 + "(测试 auto_summarize 参数)" + " " * 17 + "║")
|
||||
print("╚" + "═" * 58 + "╝")
|
||||
|
||||
print("\n请确保:")
|
||||
print("1. 后端服务正在运行 (http://localhost:8000)")
|
||||
print("2. 已修改脚本中的 AUTH_TOKEN 和 TEST_MEETING_ID")
|
||||
print("3. 已准备好测试音频文件")
|
||||
|
||||
input("\n按回车键开始测试...")
|
||||
|
||||
# 测试1: 查看当前转录状态
|
||||
test_get_transcription_status()
|
||||
|
||||
# 测试2: 查看当前LLM任务
|
||||
test_get_llm_tasks()
|
||||
|
||||
# 询问要测试哪种模式
|
||||
print("\n" + "-" * 60)
|
||||
print("请选择测试模式:")
|
||||
print("1. 仅转录 (auto_summarize=false)")
|
||||
print("2. 转录+自动总结 (auto_summarize=true)")
|
||||
print("3. 两种模式都测试")
|
||||
choice = input("请输入选项 (1/2/3): ")
|
||||
|
||||
if choice == '1':
|
||||
# 测试:仅转录
|
||||
if test_upload_audio(auto_summarize=False):
|
||||
print("\n⚠ 注意: 此模式下不会自动生成总结")
|
||||
print("如需生成总结,请手动调用: POST /meetings/{meeting_id}/generate-summary-async")
|
||||
elif choice == '2':
|
||||
# 测试:转录+自动总结
|
||||
if test_upload_audio(auto_summarize=True):
|
||||
print("\n" + "-" * 60)
|
||||
choice = input("是否要持续监控处理进度? (y/n): ")
|
||||
if choice.lower() == 'y':
|
||||
monitor_progress()
|
||||
elif choice == '3':
|
||||
# 两种模式都测试
|
||||
print("\n" + "=" * 60)
|
||||
print("测试模式1: 仅转录 (auto_summarize=false)")
|
||||
print("=" * 60)
|
||||
test_upload_audio(auto_summarize=False)
|
||||
|
||||
input("\n按回车键继续测试模式2...")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试模式2: 转录+自动总结 (auto_summarize=true)")
|
||||
print("=" * 60)
|
||||
if test_upload_audio(auto_summarize=True):
|
||||
print("\n" + "-" * 60)
|
||||
choice = input("是否要持续监控处理进度? (y/n): ")
|
||||
if choice.lower() == 'y':
|
||||
monitor_progress()
|
||||
else:
|
||||
print("\n✗ 无效选项")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试完成!")
|
||||
print("=" * 60)
|
||||
print("\n总结:")
|
||||
print("- auto_summarize=false: 只执行转录,不自动生成总结")
|
||||
print("- auto_summarize=true: 执行转录后自动生成总结")
|
||||
print("- 默认值: true (向前兼容)")
|
||||
print("- 现有页面建议设置: auto_summarize=false")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
"""
|
||||
声纹采集API测试脚本
|
||||
|
||||
使用方法:
|
||||
1. 确保后端服务正在运行
|
||||
2. 修改 USER_ID 和 TOKEN 为实际值
|
||||
3. 准备一个10秒的WAV音频文件
|
||||
4. 运行: python test_voiceprint_api.py
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
# 配置
|
||||
BASE_URL = "http://localhost:8000/api"
|
||||
USER_ID = 1 # 修改为实际用户ID
|
||||
TOKEN = "" # 登录后获取的token
|
||||
|
||||
# 请求头
|
||||
headers = {
|
||||
"Authorization": f"Bearer {TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
|
||||
def test_get_template():
|
||||
"""测试获取朗读模板"""
|
||||
print("\n=== 测试1: 获取朗读模板 ===")
|
||||
url = f"{BASE_URL}/voiceprint/template"
|
||||
response = requests.get(url, headers=headers)
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应: {json.dumps(response.json(), ensure_ascii=False, indent=2)}")
|
||||
return response.json()
|
||||
|
||||
|
||||
def test_get_status(user_id):
|
||||
"""测试获取声纹状态"""
|
||||
print(f"\n=== 测试2: 获取用户 {user_id} 的声纹状态 ===")
|
||||
url = f"{BASE_URL}/voiceprint/{user_id}"
|
||||
response = requests.get(url, headers=headers)
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应: {json.dumps(response.json(), ensure_ascii=False, indent=2)}")
|
||||
return response.json()
|
||||
|
||||
|
||||
def test_upload_voiceprint(user_id, audio_file_path):
|
||||
"""测试上传声纹"""
|
||||
print(f"\n=== 测试3: 上传声纹音频 ===")
|
||||
url = f"{BASE_URL}/voiceprint/{user_id}"
|
||||
|
||||
# 移除Content-Type,让requests自动设置multipart/form-data
|
||||
upload_headers = {
|
||||
"Authorization": f"Bearer {TOKEN}"
|
||||
}
|
||||
|
||||
with open(audio_file_path, 'rb') as f:
|
||||
files = {'audio_file': (audio_file_path.split('/')[-1], f, 'audio/wav')}
|
||||
response = requests.post(url, headers=upload_headers, files=files)
|
||||
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应: {json.dumps(response.json(), ensure_ascii=False, indent=2)}")
|
||||
return response.json()
|
||||
|
||||
|
||||
def test_delete_voiceprint(user_id):
|
||||
"""测试删除声纹"""
|
||||
print(f"\n=== 测试4: 删除用户 {user_id} 的声纹 ===")
|
||||
url = f"{BASE_URL}/voiceprint/{user_id}"
|
||||
response = requests.delete(url, headers=headers)
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应: {json.dumps(response.json(), ensure_ascii=False, indent=2)}")
|
||||
return response.json()
|
||||
|
||||
|
||||
def login(username, password):
|
||||
"""登录获取token"""
|
||||
print("\n=== 登录获取Token ===")
|
||||
url = f"{BASE_URL}/auth/login"
|
||||
data = {
|
||||
"username": username,
|
||||
"password": password
|
||||
}
|
||||
response = requests.post(url, json=data)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result.get('code') == '200':
|
||||
token = result['data']['token']
|
||||
print(f"登录成功,Token: {token[:20]}...")
|
||||
return token
|
||||
else:
|
||||
print(f"登录失败: {result.get('message')}")
|
||||
return None
|
||||
else:
|
||||
print(f"请求失败,状态码: {response.status_code}")
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("声纹采集API测试脚本")
|
||||
print("=" * 60)
|
||||
|
||||
# 步骤1: 登录(如果没有token)
|
||||
if not TOKEN:
|
||||
print("\n请先登录获取Token...")
|
||||
username = input("用户名: ")
|
||||
password = input("密码: ")
|
||||
TOKEN = login(username, password)
|
||||
if TOKEN:
|
||||
headers["Authorization"] = f"Bearer {TOKEN}"
|
||||
else:
|
||||
print("登录失败,退出测试")
|
||||
exit(1)
|
||||
|
||||
# 步骤2: 测试获取朗读模板
|
||||
test_get_template()
|
||||
|
||||
# 步骤3: 测试获取声纹状态
|
||||
test_get_status(USER_ID)
|
||||
|
||||
# 步骤4: 测试上传声纹(需要准备音频文件)
|
||||
audio_file = input("\n请输入WAV音频文件路径 (回车跳过上传测试): ")
|
||||
if audio_file.strip():
|
||||
test_upload_voiceprint(USER_ID, audio_file.strip())
|
||||
|
||||
# 上传后再次查看状态
|
||||
print("\n=== 上传后再次查看状态 ===")
|
||||
test_get_status(USER_ID)
|
||||
|
||||
# 步骤5: 测试删除声纹
|
||||
confirm = input("\n是否测试删除声纹? (yes/no): ")
|
||||
if confirm.lower() == 'yes':
|
||||
test_delete_voiceprint(USER_ID)
|
||||
|
||||
# 删除后再次查看状态
|
||||
print("\n=== 删除后再次查看状态 ===")
|
||||
test_get_status(USER_ID)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试完成")
|
||||
print("=" * 60)
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试worker线程是否正常工作
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.services.async_meeting_service import AsyncMeetingService
|
||||
|
||||
# 创建服务实例
|
||||
service = AsyncMeetingService()
|
||||
|
||||
# 直接调用处理任务方法测试
|
||||
print("测试直接调用_process_tasks方法...")
|
||||
|
||||
# 设置worker_running为True
|
||||
service.worker_running = True
|
||||
|
||||
# 创建线程并启动
|
||||
thread = threading.Thread(target=service._process_tasks)
|
||||
thread.daemon = False # 不设置为daemon,确保能看到输出
|
||||
thread.start()
|
||||
|
||||
print(f"线程是否活动: {thread.is_alive()}")
|
||||
print("等待5秒...")
|
||||
|
||||
# 等待一段时间
|
||||
time.sleep(5)
|
||||
|
||||
# 停止worker
|
||||
service.worker_running = False
|
||||
thread.join(timeout=10)
|
||||
|
||||
print("测试完成")
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
|
|
@ -56,6 +56,7 @@ const API_CONFIG = {
|
|||
USER_STATS: '/api/admin/user-stats',
|
||||
KICK_USER: (userId) => `/api/admin/kick-user/${userId}`,
|
||||
TASKS_MONITOR: '/api/admin/tasks/monitor',
|
||||
TASK_RETRY: (taskType, taskId) => `/api/admin/tasks/${taskType}/${taskId}/retry`,
|
||||
SYSTEM_RESOURCES: '/api/admin/system/resources',
|
||||
HOT_WORDS: {
|
||||
GROUPS: '/api/admin/hot-word-groups',
|
||||
|
|
|
|||
|
|
@ -227,6 +227,32 @@ export default function useAdminDashboardPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleRetryTask = async (taskRecord) => {
|
||||
if (!taskRecord?.task_id || !taskRecord?.task_type) {
|
||||
message.error('任务信息不完整');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(
|
||||
buildApiUrl(API_ENDPOINTS.ADMIN.TASK_RETRY(taskRecord.task_type, taskRecord.task_id))
|
||||
);
|
||||
message.success(response.message || '任务已提交重试');
|
||||
await fetchTasks();
|
||||
|
||||
if (selectedTaskRecord?.task_id === taskRecord.task_id) {
|
||||
setSelectedTaskRecord((prev) => (prev ? {
|
||||
...prev,
|
||||
task_id: response.data?.task_id || prev.task_id,
|
||||
status: response.data?.status || prev.status,
|
||||
progress: response.data?.progress ?? prev.progress,
|
||||
} : prev));
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error?.response?.data?.message || '任务重试失败');
|
||||
}
|
||||
};
|
||||
|
||||
const closeMeetingModal = () => {
|
||||
setShowMeetingModal(false);
|
||||
setMeetingDetails(null);
|
||||
|
|
@ -266,6 +292,7 @@ export default function useAdminDashboardPage() {
|
|||
handleDownloadTranscript,
|
||||
handleDownloadAudio,
|
||||
handleDownloadSummaryResult,
|
||||
handleRetryTask,
|
||||
closeMeetingModal,
|
||||
taskCompletionRate,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
PlayCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseOutlined,
|
||||
RedoOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import ActionButton from '../components/ActionButton';
|
||||
import useSystemPageSize from '../hooks/useSystemPageSize';
|
||||
|
|
@ -93,6 +94,7 @@ const AdminDashboard = () => {
|
|||
handleDownloadTranscript,
|
||||
handleDownloadAudio,
|
||||
handleDownloadSummaryResult,
|
||||
handleRetryTask,
|
||||
closeMeetingModal,
|
||||
taskCompletionRate,
|
||||
} = useAdminDashboardPage();
|
||||
|
|
@ -208,6 +210,22 @@ const AdminDashboard = () => {
|
|||
width: 120,
|
||||
render: (text) => (text ? new Date(text).toLocaleTimeString() : '-'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 110,
|
||||
render: (_, record) => (
|
||||
['pending', 'failed'].includes(record.status) && ['transcription', 'summary'].includes(record.task_type) ? (
|
||||
<ActionButton
|
||||
tone="view"
|
||||
variant="iconSm"
|
||||
tooltip={record.status === 'pending' ? '恢复任务' : '重试任务'}
|
||||
icon={<RedoOutlined />}
|
||||
onClick={() => handleRetryTask(record)}
|
||||
/>
|
||||
) : '-'
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -403,6 +421,7 @@ const AdminDashboard = () => {
|
|||
</div>
|
||||
) : meetingDetails ? (
|
||||
<Descriptions bordered column={1} size="small">
|
||||
<Descriptions.Item label="任务ID">{selectedTaskRecord?.task_id || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="会议名称">{meetingDetails.title}</Descriptions.Item>
|
||||
<Descriptions.Item label="关联账号">{meetingDetails.creator_account || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="开始时间">
|
||||
|
|
@ -427,6 +446,7 @@ const AdminDashboard = () => {
|
|||
</Descriptions.Item>
|
||||
) : null}
|
||||
<Descriptions.Item label="操作">
|
||||
<Space wrap>
|
||||
{selectedTaskRecord?.task_type === 'summary' ? (
|
||||
<ActionButton
|
||||
tone="view"
|
||||
|
|
@ -441,6 +461,17 @@ const AdminDashboard = () => {
|
|||
下载转录结果 (JSON)
|
||||
</ActionButton>
|
||||
)}
|
||||
{['pending', 'failed'].includes(selectedTaskRecord?.status) && ['transcription', 'summary'].includes(selectedTaskRecord?.task_type) ? (
|
||||
<ActionButton
|
||||
tone="view"
|
||||
variant="textLg"
|
||||
icon={<RedoOutlined />}
|
||||
onClick={() => handleRetryTask(selectedTaskRecord)}
|
||||
>
|
||||
{selectedTaskRecord?.status === 'pending' ? '恢复任务' : '重试任务'}
|
||||
</ActionButton>
|
||||
) : null}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
) : (
|
||||
|
|
|
|||
Loading…
Reference in New Issue