codex/dev
mula.liu 2026-04-09 22:09:33 +08:00
parent d7507e811b
commit 2c505514a5
81 changed files with 351 additions and 45071 deletions

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

146
.gitignore vendored
View File

@ -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

View File

@ -13,6 +13,9 @@ ENV/
# 用户上传文件(最重要!)
uploads/
test/
sql/
scripts/
# 测试和开发文件
test/

163
backend/.gitignore vendored
View File

@ -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/

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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)

View File

@ -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()

View File

@ -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())

View File

@ -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())

View File

@ -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` - 本说明文档

View File

@ -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='专用终端设备表';

View File

@ -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';

View File

@ -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;

View File

@ -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 表中的记录可能被删除,我们希望保留历史任务记录

View File

@ -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';

View File

@ -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
);

View File

@ -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);

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 '使用的提示词模版ID0表示未使用或使用默认模版'
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 '使用的提示词模版ID0表示未使用或使用默认模版'
AFTER tags;
-- 为 knowledge_bases 表添加索引
ALTER TABLE knowledge_bases
ADD INDEX idx_prompt_id (prompt_id);
-- ============================================
-- 验证修改
-- ============================================
-- 查看 meetings 表结构
-- DESCRIBE meetings;
-- 查看 knowledge_bases 表结构
-- DESCRIBE knowledge_bases;

View File

@ -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`;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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接入凭证';

View File

@ -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;

View File

@ -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;

View File

@ -1,8 +0,0 @@
START TRANSACTION;
UPDATE sys_menus
SET is_visible = 1,
is_active = 1
WHERE menu_code IN ('dashboard', 'desktop');
COMMIT;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 != '';

View File

@ -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;

View File

@ -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');

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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访问存在问题")

View File

@ -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}")

View File

@ -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>

View File

@ -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)

View File

@ -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=== 调试完成 ===")

View File

@ -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()

View File

@ -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)

View File

@ -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❌ 七牛云连接测试失败!")

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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请在网页上验证用户是否自动登出")

View File

@ -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()

View File

@ -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)

View File

@ -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("测试完成")

132
frontend/.gitignore vendored
View File

@ -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.*

View File

@ -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',

View File

@ -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,
};

View File

@ -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>
) : (