修正了进度展示

main
mula.liu 2026-01-09 16:06:24 +08:00
parent 5a34df3944
commit f616cb3fc3
23 changed files with 525 additions and 672 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -1,106 +0,0 @@
# 音频处理功能部署说明
## 1. 安装Python依赖
```bash
cd /Users/jiliu/工作/projects/imeeting/backend
source venv/bin/activate
pip install tinytag
```
## 2. 执行数据库迁移
### 2.1 创建用户日志表
```bash
mysql -u root -p imeeting < sql/migrations/create_user_logs_table.sql
```
### 2.2 为audio_files表添加duration字段
```bash
mysql -u root -p imeeting < sql/migrations/add_duration_to_audio_files.sql
```
## 3. 功能说明
### 3.1 用户日志表 (user_logs)
- 记录用户登录活动
- 字段log_id, user_id, action_type, ip_address, user_agent, metadata, created_at
- 管理后台可查询用户最后登录时间
### 3.2 音频处理优化
**音频时长计算:**
- 使用TinyTag库读取音频文件时长
- 纯Python实现无需外部依赖
- 支持mp3, m4a, mp4, wav, ogg, flac等常见格式
- 能处理特殊/损坏元数据的文件
- 时长保存到audio_files.duration字段单位
- 时长读取功能位于`app/utils/audio_parser.py`
### 3.3 管理后台用户统计
- 排除没有会议的用户
- 显示字段:
- ID
- 用户名
- 姓名
- 注册时间
- 最新登录时间从user_logs表查询
- 会议数量
- 会议时长从audio_files.duration汇总格式Xh Ym
## 4. 代码变更清单
### 后端变更:
- `/backend/app/utils/audio_parser.py` - 新增音频时长解析工具使用TinyTag
- `/backend/app/api/endpoints/meetings.py` - 完整文件上传时调用audio_parser获取时长修复Safari播放问题
- `/backend/app/api/endpoints/audio.py` - Stream上传完成时调用audio_parser获取时长
- `/backend/app/services/audio_service.py` - 增加duration参数
- `/backend/app/api/endpoints/auth.py` - 登录时记录日志
- `/backend/app/api/endpoints/admin_dashboard.py` - 更新用户统计查询
- `/backend/app/models/models.py` - 新增UserLog模型
### 前端变更:
- `/frontend/src/pages/HomePage.jsx` - 密码显示/隐藏切换
- `/frontend/src/pages/HomePage.css` - 密码切换按钮样式
- `/frontend/src/pages/AdminDashboard.jsx` - 用户列表增加会议时长列
- `/frontend/src/pages/MeetingPreview.jsx` - 新增转录标签页修复Safari音频播放
- `/frontend/src/pages/MeetingDetails.jsx` - 修复Safari音频播放问题
### 数据库变更:
- `sql/migrations/create_user_logs_table.sql` - 创建用户日志表
- `sql/migrations/add_duration_to_audio_files.sql` - 添加音频时长字段
## 5. 测试检查清单
- [ ] 安装Python依赖成功
- [ ] 数据库迁移执行成功
- [ ] 用户登录后user_logs表有记录
- [ ] 上传音频文件Safari可以正常播放
- [ ] 上传音频后audio_files.duration有正确的时长值
- [ ] 管理后台用户列表显示正确(排除无会议用户)
- [ ] 用户列表中会议时长统计正确
- [ ] 登录页面密码显示/隐藏按钮正常工作
- [ ] 会议预览页面音频播放正常Safari/Chrome
- [ ] 会议详情页面音频播放正常Safari/Chrome
## 6. 注意事项
1. **音频时长计算**
- 使用TinyTag纯Python库
- 无需系统依赖,跨平台兼容
- 计算失败时duration为0不影响其他功能
- 支持所有TinyTag支持的格式
2. **Safari音频播放兼容性**
- 使用直接src属性而非source子元素
- preload="metadata"模式
- 后端支持HTTP Range请求
- 正确的MIME类型由后端自动设置
3. **用户日志**
- 登录日志记录失败不影响登录流程
- IP地址优先从X-Forwarded-For获取考虑代理场景
4. **管理后台统计**
- 使用INNER JOIN过滤无会议用户
- 时长汇总从audio_files.duration计算
- 最后登录时间从user_logs子查询获取

View File

@ -1,167 +0,0 @@
# 提示词模版选择功能实现总结
## 功能概述
实现了用户在创建会议时可以选择使用的总结模版功能。支持两种场景:
1. 手动生成会议总结时选择模版
2. 上传音频文件时选择模版,自动总结时使用该模版
## 实现的功能点
### 1. 新增API接口 ✅
**文件**: `app/api/endpoints/prompts.py`
- 新增 `GET /prompts/active/{task_type}` 接口
- 功能:获取指定任务类型的所有启用状态的提示词模版
- 返回字段id, name, is_default
- 按默认模版优先、创建时间倒序排列
### 2. 修改LLM服务 ✅
**文件**: `app/services/llm_service.py`
- 修改 `get_task_prompt()` 方法,增加 `prompt_id` 可选参数
- 逻辑:
- 如果指定 prompt_id查询该ID对应的提示词需验证task_type和is_active
- 如果不指定使用默认提示词is_default=TRUE
- 如果都查不到,返回代码中的默认值
### 3. 修改会议总结服务 ✅
**文件**: `app/services/async_meeting_service.py`
#### 3.1 修改任务创建
- `start_summary_generation()`: 增加 `prompt_id` 参数
- 将 prompt_id 存储到 Redis 和数据库
#### 3.2 修改任务处理
- `_process_task()`: 从 Redis 读取 prompt_id传递给 `_build_prompt()`
- `_build_prompt()`: 增加 `prompt_id` 参数,传递给 `llm_service.get_task_prompt()`
- `_save_task_to_db()`: 增加 `prompt_id` 参数,存储到数据库
#### 3.3 修改自动总结监控
- `monitor_and_auto_summarize()`: 增加 `prompt_id` 参数
- 在转录完成后启动总结任务时,传递 prompt_id
### 4. 修改音频服务 ✅
**文件**: `app/services/audio_service.py`
- `handle_audio_upload()`: 增加 `prompt_id` 参数
- 将 prompt_id 传递给 `monitor_and_auto_summarize()`
### 5. 修改会议API接口 ✅
**文件**: `app/api/endpoints/meetings.py`
#### 5.1 手动生成总结
- `GenerateSummaryRequest` 模型:增加 `prompt_id` 字段
- `POST /meetings/{meeting_id}/generate-summary-async`: 传递 prompt_id 给服务层
#### 5.2 音频上传
- `POST /meetings/upload-audio`: 增加 `prompt_id` 表单参数
- 将 prompt_id 传递给 `handle_audio_upload()`
### 6. 数据库迁移 ✅
**文件**: `sql/add_prompt_id_to_llm_tasks.sql`
- 为 `llm_tasks` 表添加 `prompt_id`
- 类型int(11)
- 可空YES
- 默认值NULL
- 索引idx_prompt_id
## 数据流向
### 手动生成总结
```
前端 → POST /meetings/{id}/generate-summary-async (prompt_id)
→ async_meeting_service.start_summary_generation(prompt_id)
→ 存储到 Redis 和 DB (llm_tasks.prompt_id)
→ _process_task() 读取 prompt_id
→ _build_prompt(prompt_id)
→ llm_service.get_task_prompt('MEETING_TASK', prompt_id)
→ 获取指定模版或默认模版
```
### 音频上传自动总结
```
前端 → POST /meetings/upload-audio (prompt_id, auto_summarize=true)
→ handle_audio_upload(prompt_id)
→ transcription_service.start_transcription()
→ monitor_and_auto_summarize(prompt_id)
→ 等待转录完成
→ start_summary_generation(prompt_id)
→ (后续流程同手动生成总结)
```
## 向后兼容性
所有新增的 `prompt_id` 参数都是可选的Optional[int] = None确保
1. 不传递 prompt_id 时,自动使用默认模版
2. 现有代码无需修改即可正常工作
3. 数据库中 prompt_id 允许为 NULL
## 测试结果
执行 `test_prompt_id_feature.py` 测试脚本,所有测试通过:
- ✅ 获取启用的提示词列表 (6个模版)
- ✅ 通过prompt_id获取提示词内容
- ✅ 获取默认提示词不指定prompt_id
- ✅ 验证方法签名支持prompt_id参数
- ✅ 验证数据库schema包含prompt_id列
- ✅ 验证API端点定义正确
## 使用示例
### 1. 获取启用的会议任务模版列表
```bash
GET /api/prompts/active/MEETING_TASK
Authorization: Bearer <token>
```
返回:
```json
{
"code": "200",
"message": "获取启用模版列表成功",
"data": {
"prompts": [
{"id": 1, "name": "默认会议总结", "is_default": true},
{"id": 5, "name": "产品会议总结", "is_default": false}
]
}
}
```
### 2. 手动生成总结时指定模版
```bash
POST /api/meetings/123/generate-summary-async
Authorization: Bearer <token>
Content-Type: application/json
{
"user_prompt": "重点关注技术讨论",
"prompt_id": 5
}
```
### 3. 上传音频时指定模版
```bash
POST /api/meetings/upload-audio
Authorization: Bearer <token>
Content-Type: multipart/form-data
- audio_file: <file>
- meeting_id: 123
- auto_summarize: true
- prompt_id: 5
```
## 文件变更列表
1. `app/api/endpoints/prompts.py` - 新增API接口
2. `app/api/endpoints/meetings.py` - 修改两个端点
3. `app/services/llm_service.py` - 修改get_task_prompt方法
4. `app/services/async_meeting_service.py` - 修改4个方法
5. `app/services/audio_service.py` - 修改handle_audio_upload方法
6. `sql/add_prompt_id_to_llm_tasks.sql` - 数据库迁移脚本
7. `test_prompt_id_feature.py` - 测试脚本
## 注意事项
1. prompt_id 会与 task_type 一起验证,防止使用错误类型的模版
2. 如果指定的 prompt_id 不存在或未启用,会自动使用默认模版
3. 历史任务记录保留 prompt_id即使对应的提示词被删除
4. Redis 中 prompt_id 存储为字符串,使用时需转换为 int

View File

@ -1,139 +0,0 @@
# 知识库提示词模版选择功能实现总结
## 功能概述
为知识库生成功能添加了提示词模版选择支持,用户在创建知识库时可以选择使用的生成模版。
## 实现的功能点
### 1. 修改请求模型 ✅
**文件**: `app/models/models.py`
- `CreateKnowledgeBaseRequest` 模型增加 `prompt_id` 字段
- 类型Optional[int] = None
- 不指定时使用默认模版
### 2. 修改知识库异步服务 ✅
**文件**: `app/services/async_knowledge_base_service.py`
#### 2.1 修改任务创建
- `start_generation()`: 增加 `prompt_id` 参数
- 将 prompt_id 存储到 Redis 和数据库
- 支持通过 cursor 参数直接插入(事务场景)
#### 2.2 修改任务处理
- `_process_task()`: 从 Redis 读取 prompt_id传递给 `_build_prompt()`
- 处理空字符串情况,转换为 None
#### 2.3 修改提示词构建
- `_build_prompt()`: 增加 `prompt_id` 参数
- 调用 `llm_service.get_task_prompt('KNOWLEDGE_TASK', prompt_id=prompt_id)`
- 支持获取指定模版或默认模版
#### 2.4 修改数据库保存
- `_save_task_to_db()`: 增加 `prompt_id` 参数
- 插入时包含 prompt_id 字段
### 3. 修改API接口 ✅
**文件**: `app/api/endpoints/knowledge_base.py`
- `create_knowledge_base`: 从请求中获取 `prompt_id`
- 调用 `async_kb_service.start_generation()` 时传递 `prompt_id`
### 4. 数据库字段 ✅
- `knowledge_base_tasks` 表已包含 `prompt_id` 列(用户已添加)
- 类型int
- 可空NO默认值0
## 数据流向
```
前端 → POST /api/knowledge-bases (prompt_id)
→ CreateKnowledgeBaseRequest (prompt_id)
→ async_kb_service.start_generation(prompt_id)
→ 存储到 Redis 和 DB (knowledge_base_tasks.prompt_id)
→ _process_task() 读取 prompt_id
→ _build_prompt(prompt_id)
→ llm_service.get_task_prompt('KNOWLEDGE_TASK', prompt_id)
→ 获取指定模版或默认模版
```
## 向后兼容性
所有新增的 `prompt_id` 参数都是可选的Optional[int] = None确保
1. 不传递 prompt_id 时,自动使用默认模版
2. 现有代码无需修改即可正常工作
3. 数据库中 prompt_id 有默认值 0
## 测试结果
执行 `test_kb_prompt_id_feature.py` 测试脚本,所有测试通过:
- ✅ 获取启用的知识库提示词列表 (3个模版)
- ✅ 通过prompt_id获取提示词内容
- ✅ 获取默认提示词不指定prompt_id
- ✅ 验证方法签名支持prompt_id参数
- ✅ 验证数据库schema包含prompt_id列
- ✅ 验证API模型定义正确
## 使用示例
### 1. 获取启用的知识库任务模版列表
```bash
GET /api/prompts/active/KNOWLEDGE_TASK
Authorization: Bearer <token>
```
返回:
```json
{
"code": "200",
"message": "获取启用模版列表成功",
"data": {
"prompts": [
{"id": 2, "name": "默认知识库生成", "is_default": true},
{"id": 13, "name": "分析总结模版", "is_default": false}
]
}
}
```
### 2. 创建知识库时指定模版
```bash
POST /api/knowledge-bases
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "产品会议知识库",
"is_shared": false,
"user_prompt": "重点提取产品功能相关信息",
"source_meeting_ids": "1,2,3",
"tags": "产品,功能",
"prompt_id": 13
}
```
## 文件变更列表
1. `app/models/models.py` - 修改CreateKnowledgeBaseRequest模型
2. `app/services/async_knowledge_base_service.py` - 修改5个方法
3. `app/api/endpoints/knowledge_base.py` - 修改create_knowledge_base端点
4. `test_kb_prompt_id_feature.py` - 测试脚本
## 与会议总结功能的一致性
知识库的实现与会议总结功能保持一致:
- 相同的prompt_id传递机制
- 相同的Redis存储格式字符串
- 相同的数据库字段类型
- 相同的向后兼容策略
- 相同的验证逻辑task_type + is_active
## 注意事项
1. prompt_id 会与 task_type='KNOWLEDGE_TASK' 一起验证
2. 如果指定的 prompt_id 不存在或未启用,会自动使用默认模版
3. 历史任务记录保留 prompt_id即使对应的提示词被删除
4. Redis 中 prompt_id 存储为字符串,使用时需转换为 int
5. 数据库 prompt_id 默认值为 0表示未指定
## 总结
知识库提示词模版选择功能已完全实现并通过测试,与会议总结功能保持一致的设计和实现方式。用户现在可以在创建知识库时选择不同的生成模版,以满足不同场景的需求。

BIN
app.zip

Binary file not shown.

View File

@ -1,126 +1,12 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from app.core.auth import get_current_admin_user, get_current_user from app.core.auth import get_current_admin_user, get_current_user
from app.core.config import LLM_CONFIG, DEFAULT_RESET_PASSWORD, MAX_FILE_SIZE, VOICEPRINT_CONFIG, TIMELINE_PAGESIZE
from app.core.response import create_api_response from app.core.response import create_api_response
from app.core.database import get_db_connection from app.core.database import get_db_connection
from app.models.models import MenuInfo, MenuListResponse, RolePermissionInfo, UpdateRolePermissionsRequest, RoleInfo from app.models.models import MenuInfo, MenuListResponse, RolePermissionInfo, UpdateRolePermissionsRequest, RoleInfo
from pydantic import BaseModel
from typing import List from typing import List
import json
from pathlib import Path
router = APIRouter() router = APIRouter()
# 配置文件路径
CONFIG_FILE = Path(__file__).parent.parent.parent.parent / "config" / "system_config.json"
class SystemConfigModel(BaseModel):
model_name: str
template_text: str
DEFAULT_RESET_PASSWORD: str
MAX_FILE_SIZE: int # 字节为单位
TIMELINE_PAGESIZE: int # 分页数量
def load_config_from_file():
"""从文件加载配置,如果文件不存在则返回默认配置"""
try:
if CONFIG_FILE.exists():
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
pass
# 返回默认配置
return {
'model_name': LLM_CONFIG['model_name'],
'template_text': VOICEPRINT_CONFIG['template_text'],
'DEFAULT_RESET_PASSWORD': DEFAULT_RESET_PASSWORD,
'MAX_FILE_SIZE': MAX_FILE_SIZE,
'TIMELINE_PAGESIZE': TIMELINE_PAGESIZE
}
def save_config_to_file(config_data):
"""将配置保存到文件"""
try:
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config_data, f, ensure_ascii=False, indent=2)
return True
except Exception as e:
print(f"保存配置文件失败: {e}")
return False
@router.get("/admin/system-config")
async def get_system_config(current_user=Depends(get_current_user)):
"""
获取系统配置
普通用户也可以获取
"""
try:
config = load_config_from_file()
response_data = {
'model_name': config.get('model_name', LLM_CONFIG['model_name']),
'template_text': config.get('template_text', VOICEPRINT_CONFIG['template_text']),
'DEFAULT_RESET_PASSWORD': config.get('DEFAULT_RESET_PASSWORD', DEFAULT_RESET_PASSWORD),
'MAX_FILE_SIZE': config.get('MAX_FILE_SIZE', MAX_FILE_SIZE),
'TIMELINE_PAGESIZE': config.get('TIMELINE_PAGESIZE', TIMELINE_PAGESIZE),
}
return create_api_response(code="200", message="配置获取成功", data=response_data)
except Exception as e:
return create_api_response(code="500", message=f"获取配置失败: {str(e)}")
@router.put("/admin/system-config")
async def update_system_config(
config: SystemConfigModel,
current_user=Depends(get_current_admin_user)
):
"""
更新系统配置
只有管理员才能访问
"""
try:
config_data = {
'model_name': config.model_name,
'template_text': config.template_text,
'DEFAULT_RESET_PASSWORD': config.DEFAULT_RESET_PASSWORD,
'MAX_FILE_SIZE': config.MAX_FILE_SIZE,
'TIMELINE_PAGESIZE': config.TIMELINE_PAGESIZE
}
if not save_config_to_file(config_data):
return create_api_response(code="500", message="配置保存到文件失败")
# 更新运行时配置
LLM_CONFIG['model_name'] = config.model_name
VOICEPRINT_CONFIG['template_text'] = config.template_text
import app.core.config as config_module
config_module.DEFAULT_RESET_PASSWORD = config.DEFAULT_RESET_PASSWORD
config_module.MAX_FILE_SIZE = config.MAX_FILE_SIZE
config_module.TIMELINE_PAGESIZE = config.TIMELINE_PAGESIZE
return create_api_response(
code="200",
message="配置更新成功,重启服务后完全生效",
data=config_data
)
except Exception as e:
return create_api_response(code="500", message=f"更新配置失败: {str(e)}")
# 在应用启动时加载配置
def load_system_config():
"""在应用启动时调用,加载保存的配置"""
try:
config = load_config_from_file()
LLM_CONFIG['model_name'] = config.get('model_name', LLM_CONFIG['model_name'])
VOICEPRINT_CONFIG['template_text'] = config.get('template_text', VOICEPRINT_CONFIG['template_text'])
import app.core.config as config_module
config_module.DEFAULT_RESET_PASSWORD = config.get('DEFAULT_RESET_PASSWORD', DEFAULT_RESET_PASSWORD)
config_module.MAX_FILE_SIZE = config.get('MAX_FILE_SIZE', MAX_FILE_SIZE)
config_module.TIMELINE_PAGESIZE = config.get('TIMELINE_PAGESIZE', TIMELINE_PAGESIZE)
print(f"系统配置加载成功: model={config.get('model_name')}, pagesize={config.get('TIMELINE_PAGESIZE')}")
except Exception as e:
print(f"加载系统配置失败,使用默认配置: {e}")
# ========== 菜单权限管理接口 ========== # ========== 菜单权限管理接口 ==========
@router.get("/admin/menus") @router.get("/admin/menus")

View File

@ -25,7 +25,7 @@ TEMP_UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
# 配置常量 # 配置常量
MAX_CHUNK_SIZE = 2 * 1024 * 1024 # 2MB per chunk MAX_CHUNK_SIZE = 2 * 1024 * 1024 # 2MB per chunk
MAX_TOTAL_SIZE = 500 * 1024 * 1024 # 500MB total MAX_TOTAL_SIZE = 500 * 1024 * 1024 # 500MB total (流式上传不受系统参数控制)
MAX_DURATION = 3600 # 1 hour max recording MAX_DURATION = 3600 # 1 hour max recording
SESSION_EXPIRE_HOURS = 1 # 会话1小时后过期 SESSION_EXPIRE_HOURS = 1 # 会话1小时后过期

View File

@ -167,7 +167,7 @@ async def get_dict_data_by_code(dict_type: str, dict_code: str):
query = """ query = """
SELECT id, dict_type, dict_code, parent_code, tree_path, SELECT id, dict_type, dict_code, parent_code, tree_path,
label_cn, label_en, sort_order, extension_attr, label_cn, label_en, sort_order, extension_attr,
is_default, status, create_time is_default, status, create_time, update_time
FROM dict_data FROM dict_data
WHERE dict_type = %s AND dict_code = %s WHERE dict_type = %s AND dict_code = %s
LIMIT 1 LIMIT 1
@ -211,6 +211,17 @@ async def create_dict_data(
创建码表数据仅管理员 创建码表数据仅管理员
""" """
try: try:
# 验证extension_attr的JSON格式
if request.extension_attr:
try:
# 尝试序列化验证是否为有效的JSON对象
json.dumps(request.extension_attr)
except (TypeError, ValueError) as e:
return create_api_response(
code="400",
message=f"扩展属性格式错误必须是有效的JSON对象: {str(e)}"
)
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
@ -233,7 +244,7 @@ async def create_dict_data(
sort_order, extension_attr, is_default, status sort_order, extension_attr, is_default, status
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
""" """
extension_json = json.dumps(request.extension_attr) if request.extension_attr else None extension_json = json.dumps(request.extension_attr, ensure_ascii=False) if request.extension_attr else None
cursor.execute(query, ( cursor.execute(query, (
request.dict_type, request.dict_type,
@ -274,6 +285,17 @@ async def update_dict_data(
更新码表数据仅管理员 更新码表数据仅管理员
""" """
try: try:
# 验证extension_attr的JSON格式
if request.extension_attr is not None:
try:
# 尝试序列化验证是否为有效的JSON对象
json.dumps(request.extension_attr)
except (TypeError, ValueError) as e:
return create_api_response(
code="400",
message=f"扩展属性格式错误必须是有效的JSON对象: {str(e)}"
)
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(dictionary=True)
@ -309,7 +331,7 @@ async def update_dict_data(
if request.extension_attr is not None: if request.extension_attr is not None:
update_fields.append("extension_attr = %s") update_fields.append("extension_attr = %s")
params.append(json.dumps(request.extension_attr)) params.append(json.dumps(request.extension_attr, ensure_ascii=False))
if request.is_default is not None: if request.is_default is not None:
update_fields.append("is_default = %s") update_fields.append("is_default = %s")

View File

@ -0,0 +1,173 @@
from fastapi import APIRouter, Depends, HTTPException
from app.core.database import get_db_connection
from app.core.auth import get_current_admin_user
from app.core.response import create_api_response
from app.core.config import QWEN_API_KEY
from app.services.system_config_service import SystemConfigService
from pydantic import BaseModel
from typing import Optional, List
import json
import dashscope
from dashscope.audio.asr import VocabularyService
from datetime import datetime
from http import HTTPStatus
router = APIRouter()
class HotWordItem(BaseModel):
id: int
text: str
weight: int
lang: str
status: int
create_time: datetime
update_time: datetime
class CreateHotWordRequest(BaseModel):
text: str
weight: int = 4
lang: str = "zh"
status: int = 1
class UpdateHotWordRequest(BaseModel):
text: Optional[str] = None
weight: Optional[int] = None
lang: Optional[str] = None
status: Optional[int] = None
@router.get("/admin/hot-words", response_model=dict)
async def list_hot_words(current_user: dict = Depends(get_current_admin_user)):
"""获取热词列表"""
try:
with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT * FROM hot_words ORDER BY update_time DESC")
items = cursor.fetchall()
cursor.close()
return create_api_response(code="200", message="获取成功", data=items)
except Exception as e:
return create_api_response(code="500", message=f"获取失败: {str(e)}")
@router.post("/admin/hot-words", response_model=dict)
async def create_hot_word(request: CreateHotWordRequest, current_user: dict = Depends(get_current_admin_user)):
"""创建热词"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
query = "INSERT INTO hot_words (text, weight, lang, status) VALUES (%s, %s, %s, %s)"
cursor.execute(query, (request.text, request.weight, request.lang, request.status))
new_id = cursor.lastrowid
conn.commit()
cursor.close()
return create_api_response(code="200", message="创建成功", data={"id": new_id})
except Exception as e:
return create_api_response(code="500", message=f"创建失败: {str(e)}")
@router.put("/admin/hot-words/{id}", response_model=dict)
async def update_hot_word(id: int, request: UpdateHotWordRequest, current_user: dict = Depends(get_current_admin_user)):
"""更新热词"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
update_fields = []
params = []
if request.text is not None:
update_fields.append("text = %s")
params.append(request.text)
if request.weight is not None:
update_fields.append("weight = %s")
params.append(request.weight)
if request.lang is not None:
update_fields.append("lang = %s")
params.append(request.lang)
if request.status is not None:
update_fields.append("status = %s")
params.append(request.status)
if not update_fields:
return create_api_response(code="400", message="无更新内容")
query = f"UPDATE hot_words SET {', '.join(update_fields)} WHERE id = %s"
params.append(id)
cursor.execute(query, tuple(params))
conn.commit()
cursor.close()
return create_api_response(code="200", message="更新成功")
except Exception as e:
return create_api_response(code="500", message=f"更新失败: {str(e)}")
@router.delete("/admin/hot-words/{id}", response_model=dict)
async def delete_hot_word(id: int, current_user: dict = Depends(get_current_admin_user)):
"""删除热词"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM hot_words WHERE id = %s", (id,))
conn.commit()
cursor.close()
return create_api_response(code="200", message="删除成功")
except Exception as e:
return create_api_response(code="500", message=f"删除失败: {str(e)}")
@router.post("/admin/hot-words/sync", response_model=dict)
async def sync_hot_words(current_user: dict = Depends(get_current_admin_user)):
"""同步热词到阿里云 DashScope"""
try:
dashscope.api_key = QWEN_API_KEY
# 1. 获取所有启用的热词
with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT text, weight, lang FROM hot_words WHERE status = 1")
hot_words = cursor.fetchall()
cursor.close()
# 2. 获取现有的 vocabulary_id
existing_vocab_id = SystemConfigService.get_asr_vocabulary_id()
# 构建热词列表
vocabulary_list = [{"text": hw['text'], "weight": hw['weight'], "lang": hw['lang']} for hw in hot_words]
if not vocabulary_list:
return create_api_response(code="400", message="没有启用的热词可同步")
# 3. 调用阿里云 API
service = VocabularyService()
vocab_id = existing_vocab_id
try:
if existing_vocab_id:
# 尝试更新现有的热词表
try:
service.update_vocabulary(
vocabulary_id=existing_vocab_id,
vocabulary=vocabulary_list
)
# 更新成功保持原有ID
except Exception as update_error:
# 如果更新失败(如资源不存在),尝试创建新的
print(f"Update vocabulary failed: {update_error}, trying to create new one.")
existing_vocab_id = None # 重置,触发创建逻辑
if not existing_vocab_id:
# 创建新的热词表
vocab_id = service.create_vocabulary(
prefix='imeeting',
target_model='paraformer-v2',
vocabulary=vocabulary_list
)
except Exception as api_error:
return create_api_response(code="500", message=f"同步到阿里云失败: {str(api_error)}")
# 4. 更新数据库中的 vocabulary_id
if vocab_id:
SystemConfigService.set_config(
SystemConfigService.ASR_VOCABULARY_ID,
vocab_id
)
return create_api_response(code="200", message="同步成功", data={"vocabulary_id": vocab_id})
except Exception as e:
return create_api_response(code="500", message=f"同步异常: {str(e)}")

View File

@ -8,6 +8,7 @@ from app.services.llm_service import LLMService
from app.services.async_transcription_service import AsyncTranscriptionService from app.services.async_transcription_service import AsyncTranscriptionService
from app.services.async_meeting_service import async_meeting_service from app.services.async_meeting_service import async_meeting_service
from app.services.audio_service import handle_audio_upload from app.services.audio_service import handle_audio_upload
from app.services.system_config_service import SystemConfigService
from app.utils.audio_parser import get_audio_duration from app.utils.audio_parser import get_audio_duration
from app.core.auth import get_current_user, get_optional_current_user from app.core.auth import get_current_user, get_optional_current_user
from app.core.response import create_api_response from app.core.response import create_api_response
@ -143,11 +144,9 @@ def get_meetings(
tags: Optional[str] = None, tags: Optional[str] = None,
filter_type: str = "all" filter_type: str = "all"
): ):
from app.core.config import TIMELINE_PAGESIZE
# 使用配置的默认页面大小 # 使用配置的默认页面大小
if page_size is None: if page_size is None:
page_size = TIMELINE_PAGESIZE page_size = SystemConfigService.get_timeline_pagesize(default=10)
with get_db_connection() as connection: with get_db_connection() as connection:
cursor = connection.cursor(dictionary=True) cursor = connection.cursor(dictionary=True)
@ -486,8 +485,14 @@ async def upload_audio(
""" """
auto_summarize_bool = auto_summarize.lower() in ("true", "1", "yes") auto_summarize_bool = auto_summarize.lower() in ("true", "1", "yes")
# 打印接收到的 prompt_id # 0. 如果没有传入 prompt_id尝试获取默认模版ID
print(f"[Upload Audio] Meeting ID: {meeting_id}, Received prompt_id: {prompt_id}, Type: {type(prompt_id)}, Auto-summarize: {auto_summarize_bool}") if prompt_id is None:
with get_db_connection() as connection:
cursor = connection.cursor()
cursor.execute(
"SELECT id FROM prompts WHERE task_type = 'MEETING_TASK' AND is_default = TRUE AND is_active = TRUE LIMIT 1"
)
prompt_id = cursor.fetchone()[0]
# 1. 文件类型验证 # 1. 文件类型验证
file_extension = os.path.splitext(audio_file.filename)[1].lower() file_extension = os.path.splitext(audio_file.filename)[1].lower()
@ -498,7 +503,7 @@ async def upload_audio(
) )
# 2. 文件大小验证 # 2. 文件大小验证
max_file_size = getattr(config_module, 'MAX_FILE_SIZE', 100 * 1024 * 1024) max_file_size = SystemConfigService.get_max_audio_size(default=100) * 1024 * 1024 # MB转字节
if audio_file.size > max_file_size: if audio_file.size > max_file_size:
return create_api_response( return create_api_response(
code="400", code="400",
@ -987,6 +992,7 @@ def get_meeting_preview_data(meeting_id: int):
- 200: 会议已完成summary已生成 - 200: 会议已完成summary已生成
- 400: 会议处理中转译或总结阶段 - 400: 会议处理中转译或总结阶段
- 503: 处理失败转译或总结失败 - 503: 处理失败转译或总结失败
- 504: 数据异常流程完成但summary未生成
- 404: 会议不存在 - 404: 会议不存在
""" """
try: try:
@ -1049,7 +1055,19 @@ def get_meeting_preview_data(meeting_id: int):
} }
) )
# 情况3: 全部完成 → 返回200提供完整预览数据 # 情况3: 全部完成但Summary缺失 → 返回504
if overall_status == "completed" and not meeting['summary']:
return create_api_response(
code="504",
message="处理已完成AI总结尚未同步请稍后重试",
data={
"meeting_id": meeting_id,
"title": meeting['title'],
"processing_status": progress_info
}
)
# 情况4: 全部完成 → 返回200提供完整预览数据
if overall_status == "completed" and meeting['summary']: if overall_status == "completed" and meeting['summary']:
# 获取参会人员信息 # 获取参会人员信息
with get_db_connection() as connection: with get_db_connection() as connection:

View File

@ -4,6 +4,7 @@ from app.models.models import UserInfo, PasswordChangeRequest, UserListResponse,
from app.core.database import get_db_connection from app.core.database import get_db_connection
from app.core.auth import get_current_user from app.core.auth import get_current_user
from app.core.response import create_api_response from app.core.response import create_api_response
from app.services.system_config_service import SystemConfigService
import app.core.config as config_module import app.core.config as config_module
import hashlib import hashlib
import datetime import datetime
@ -46,7 +47,7 @@ def create_user(request: CreateUserRequest, current_user: dict = Depends(get_cur
if cursor.fetchone(): if cursor.fetchone():
return create_api_response(code="400", message="用户名已存在") return create_api_response(code="400", message="用户名已存在")
password = request.password if request.password else config_module.DEFAULT_RESET_PASSWORD password = request.password if request.password else SystemConfigService.get_default_reset_password()
hashed_password = hash_password(password) hashed_password = hash_password(password)
query = "INSERT INTO users (username, password_hash, caption, email, role_id, created_at) VALUES (%s, %s, %s, %s, %s, %s)" query = "INSERT INTO users (username, password_hash, caption, email, role_id, created_at) VALUES (%s, %s, %s, %s, %s, %s)"
@ -138,7 +139,7 @@ def reset_password(user_id: int, current_user: dict = Depends(get_current_user))
if not cursor.fetchone(): if not cursor.fetchone():
return create_api_response(code="404", message="用户不存在") return create_api_response(code="404", message="用户不存在")
hashed_password = hash_password(config_module.DEFAULT_RESET_PASSWORD) hashed_password = hash_password(SystemConfigService.get_default_reset_password())
query = "UPDATE users SET password_hash = %s WHERE user_id = %s" query = "UPDATE users SET password_hash = %s WHERE user_id = %s"
cursor.execute(query, (hashed_password, user_id)) cursor.execute(query, (hashed_password, user_id))
@ -162,7 +163,7 @@ def get_all_users(
count_params = [] count_params = []
if role_id is not None: if role_id is not None:
where_conditions.append("role_id = %s") where_conditions.append("u.role_id = %s")
count_params.append(role_id) count_params.append(role_id)
if search: if search:
@ -171,7 +172,7 @@ def get_all_users(
count_params.extend([search_pattern, search_pattern]) count_params.extend([search_pattern, search_pattern])
# 统计查询 # 统计查询
count_query = "SELECT COUNT(*) as total FROM users" count_query = "SELECT COUNT(*) as total FROM users u"
if where_conditions: if where_conditions:
count_query += " WHERE " + " AND ".join(where_conditions) count_query += " WHERE " + " AND ".join(where_conditions)

View File

@ -10,6 +10,7 @@ from app.models.models import VoiceprintStatus, VoiceprintTemplate
from app.core.auth import get_current_user from app.core.auth import get_current_user
from app.core.response import create_api_response from app.core.response import create_api_response
from app.services.voiceprint_service import voiceprint_service from app.services.voiceprint_service import voiceprint_service
from app.services.system_config_service import SystemConfigService
import app.core.config as config_module import app.core.config as config_module
router = APIRouter() router = APIRouter()
@ -23,7 +24,16 @@ def get_voiceprint_template(current_user: dict = Depends(get_current_user)):
权限需要登录 权限需要登录
""" """
try: try:
template_data = VoiceprintTemplate(**config_module.VOICEPRINT_CONFIG) # 动态从数据库获取声纹模版
template_text = SystemConfigService.get_voiceprint_template(
default="我正在进行声纹采集,这段语音将用于身份识别和验证。\n\n声纹技术能够准确识别每个人独特的声音特征。"
)
template_data = VoiceprintTemplate(
template_text=template_text,
duration_seconds=config_module.VOICEPRINT_CONFIG.get('duration_seconds', 12),
sample_rate=config_module.VOICEPRINT_CONFIG.get('sample_rate', 16000),
channels=config_module.VOICEPRINT_CONFIG.get('channels', 1)
)
return create_api_response(code="200", message="获取朗读模板成功", data=template_data.dict()) return create_api_response(code="200", message="获取朗读模板成功", data=template_data.dict())
except Exception as e: except Exception as e:
return create_api_response(code="500", message=f"获取朗读模板失败: {str(e)}") return create_api_response(code="500", message=f"获取朗读模板失败: {str(e)}")

View File

@ -29,10 +29,10 @@ CLIENT_DIR.mkdir(exist_ok=True)
# 数据库配置 # 数据库配置
DATABASE_CONFIG = { DATABASE_CONFIG = {
'host': os.getenv('DB_HOST', '10.100.51.161'), 'host': os.getenv('DB_HOST', '10.100.51.51'),
'user': os.getenv('DB_USER', 'root'), 'user': os.getenv('DB_USER', 'root'),
'password': os.getenv('DB_PASSWORD', 'sagacity'), 'password': os.getenv('DB_PASSWORD', 'Unis@123'),
'database': os.getenv('DB_NAME', 'imeeting'), 'database': os.getenv('DB_NAME', 'imeeting_dev'),
'port': int(os.getenv('DB_PORT', '3306')), 'port': int(os.getenv('DB_PORT', '3306')),
'charset': 'utf8mb4' 'charset': 'utf8mb4'
} }
@ -56,10 +56,10 @@ APP_CONFIG = {
# Redis配置 # Redis配置
REDIS_CONFIG = { REDIS_CONFIG = {
'host': os.getenv('REDIS_HOST', '10.100.51.161'), 'host': os.getenv('REDIS_HOST', '10.100.51.51'),
'port': int(os.getenv('REDIS_PORT', '6379')), 'port': int(os.getenv('REDIS_PORT', '6379')),
'db': int(os.getenv('REDIS_DB', '0')), 'db': int(os.getenv('REDIS_DB', '0')),
'password': os.getenv('REDIS_PASSWORD', ''), 'password': os.getenv('REDIS_PASSWORD', 'Unis@123'),
'decode_responses': True 'decode_responses': True
} }

View File

@ -13,9 +13,8 @@ import uvicorn
from fastapi import FastAPI, Request, HTTPException from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data, hot_words
from app.core.config import UPLOAD_DIR, API_CONFIG from app.core.config import UPLOAD_DIR, API_CONFIG
from app.api.endpoints.admin import load_system_config
app = FastAPI( app = FastAPI(
title="iMeeting API", title="iMeeting API",
@ -23,9 +22,6 @@ app = FastAPI(
version="1.0.2" version="1.0.2"
) )
# 加载系统配置
load_system_config()
# 添加CORS中间件 # 添加CORS中间件
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@ -53,6 +49,7 @@ app.include_router(client_downloads.router, prefix="/api", tags=["ClientDownload
app.include_router(dict_data.router, prefix="/api", tags=["DictData"]) app.include_router(dict_data.router, prefix="/api", tags=["DictData"])
app.include_router(voiceprint.router, prefix="/api", tags=["Voiceprint"]) app.include_router(voiceprint.router, prefix="/api", tags=["Voiceprint"])
app.include_router(audio.router, prefix="/api", tags=["Audio"]) app.include_router(audio.router, prefix="/api", tags=["Audio"])
app.include_router(hot_words.router, prefix="/api", tags=["HotWords"])
@app.get("/") @app.get("/")
def read_root(): def read_root():

View File

@ -404,23 +404,6 @@ class AsyncMeetingService:
try: try:
with get_db_connection() as connection: with get_db_connection() as connection:
cursor = connection.cursor() cursor = connection.cursor()
# 如果没有指定 prompt_id获取默认的会议总结模版ID
if prompt_id is None:
print(f"[Meeting Service] prompt_id is None, fetching default template for MEETING_TASK")
cursor.execute(
"SELECT id FROM prompts WHERE task_type = 'MEETING_TASK' AND is_default = TRUE AND is_active = TRUE LIMIT 1"
)
default_prompt = cursor.fetchone()
if default_prompt:
prompt_id = default_prompt[0]
print(f"[Meeting Service] Found default template ID: {prompt_id}")
else:
print(f"[Meeting Service] WARNING: No default template found for MEETING_TASK!")
else:
print(f"[Meeting Service] Using provided prompt_id: {prompt_id}")
print(f"[Meeting Service] Inserting task into llm_tasks - task_id: {task_id}, meeting_id: {meeting_id}, prompt_id: {prompt_id}")
insert_query = "INSERT INTO llm_tasks (task_id, meeting_id, user_prompt, prompt_id, status, progress, created_at) VALUES (%s, %s, %s, %s, 'pending', 0, NOW())" insert_query = "INSERT INTO llm_tasks (task_id, meeting_id, user_prompt, prompt_id, status, progress, created_at) VALUES (%s, %s, %s, %s, 'pending', 0, NOW())"
cursor.execute(insert_query, (task_id, meeting_id, user_prompt, prompt_id)) cursor.execute(insert_query, (task_id, meeting_id, user_prompt, prompt_id))
connection.commit() connection.commit()

View File

@ -11,6 +11,7 @@ from dashscope.audio.asr import Transcription
from app.core.config import QWEN_API_KEY, REDIS_CONFIG, APP_CONFIG from app.core.config import QWEN_API_KEY, REDIS_CONFIG, APP_CONFIG
from app.core.database import get_db_connection from app.core.database import get_db_connection
from app.services.system_config_service import SystemConfigService
class AsyncTranscriptionService: class AsyncTranscriptionService:
@ -58,17 +59,24 @@ class AsyncTranscriptionService:
# 2. 构造完整的文件URL # 2. 构造完整的文件URL
file_url = f"{self.base_url}{audio_file_path}" file_url = f"{self.base_url}{audio_file_path}"
print(f"Starting transcription for meeting_id: {meeting_id}, file_url: {file_url}") # 获取热词表ID (asr_vocabulary_id)
vocabulary_id = SystemConfigService.get_asr_vocabulary_id()
print(f"Starting transcription for meeting_id: {meeting_id}, file_url: {file_url}, vocabulary_id: {vocabulary_id}")
# 3. 调用Paraformer异步API # 3. 调用Paraformer异步API
task_response = Transcription.async_call( call_params = {
model='paraformer-v2', 'model': 'paraformer-v2',
file_urls=[file_url], 'file_urls': [file_url],
language_hints=['zh', 'en'], 'language_hints': ['zh', 'en'],
disfluency_removal_enabled=True, 'disfluency_removal_enabled': True,
diarization_enabled=True, 'diarization_enabled': True,
speaker_count=10 'speaker_count': 10
) }
if vocabulary_id:
call_params['vocabulary_id'] = vocabulary_id
task_response = Transcription.async_call(**call_params)
if task_response.status_code != HTTPStatus.OK: if task_response.status_code != HTTPStatus.OK:
print(f"Failed to start transcription: {task_response.status_code}, {task_response.message}") print(f"Failed to start transcription: {task_response.status_code}, {task_response.message}")

View File

@ -4,6 +4,7 @@ from http import HTTPStatus
from typing import Optional, Dict, List, Generator from typing import Optional, Dict, List, Generator
import app.core.config as config_module import app.core.config as config_module
from app.core.database import get_db_connection from app.core.database import get_db_connection
from app.services.system_config_service import SystemConfigService
class LLMService: class LLMService:
@ -16,27 +17,28 @@ class LLMService:
@property @property
def model_name(self): def model_name(self):
"""动态获取模型名称""" """动态获取模型名称"""
return config_module.LLM_CONFIG["model_name"] return SystemConfigService.get_llm_model_name(default="qwen-plus")
@property @property
def system_prompt(self): def system_prompt(self):
"""动态获取系统提示词""" """动态获取系统提示词fallback优先使用prompts表"""
return config_module.LLM_CONFIG["system_prompt"] # 保留config中的system_prompt作为后备
return config_module.LLM_CONFIG.get("system_prompt", "请根据提供的内容进行总结和分析。")
@property @property
def time_out(self): def time_out(self):
"""动态获取超时时间""" """动态获取超时时间"""
return config_module.LLM_CONFIG["time_out"] return SystemConfigService.get_llm_timeout(default=120)
@property @property
def temperature(self): def temperature(self):
"""动态获取temperature""" """动态获取temperature"""
return config_module.LLM_CONFIG["temperature"] return SystemConfigService.get_llm_temperature(default=0.7)
@property @property
def top_p(self): def top_p(self):
"""动态获取top_p""" """动态获取top_p"""
return config_module.LLM_CONFIG["top_p"] return SystemConfigService.get_llm_top_p(default=0.9)
def get_task_prompt(self, task_type: str, cursor=None, prompt_id: Optional[int] = None) -> str: def get_task_prompt(self, task_type: str, cursor=None, prompt_id: Optional[int] = None) -> str:
""" """

View File

@ -0,0 +1,237 @@
import json
from typing import Optional, Dict, Any
from app.core.database import get_db_connection
class SystemConfigService:
"""系统配置服务 - 从 dict_data 表中读取和保存 system_config 类型的配置"""
DICT_TYPE = 'system_config'
# 配置键常量
ASR_VOCABULARY_ID = 'asr_vocabulary_id'
VOICEPRINT_TEMPLATE = 'voiceprint_template'
TIMELINE_PAGESIZE = 'timeline_pagesize'
DEFAULT_RESET_PASSWORD = 'default_reset_password'
MAX_AUDIO_SIZE = 'max_audio_size'
# LLM模型配置
LLM_MODEL_NAME = 'llm_model_name'
LLM_TIMEOUT = 'llm_timeout'
LLM_TEMPERATURE = 'llm_temperature'
LLM_TOP_P = 'llm_top_p'
@classmethod
def get_config(cls, dict_code: str, default_value: Any = None) -> Any:
"""
获取指定配置项的值
Args:
dict_code: 配置项编码
default_value: 默认值如果配置不存在则返回此值
Returns:
配置项的值
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True)
query = """
SELECT extension_attr
FROM dict_data
WHERE dict_type = %s AND dict_code = %s AND status = 1
LIMIT 1
"""
cursor.execute(query, (cls.DICT_TYPE, dict_code))
result = cursor.fetchone()
cursor.close()
if result and result['extension_attr']:
try:
ext_attr = json.loads(result['extension_attr']) if isinstance(result['extension_attr'], str) else result['extension_attr']
return ext_attr.get('value', default_value)
except (json.JSONDecodeError, AttributeError):
pass
return default_value
except Exception as e:
print(f"Error getting config {dict_code}: {e}")
return default_value
@classmethod
def set_config(cls, dict_code: str, value: Any, label_cn: str = None) -> bool:
"""
设置指定配置项的值
Args:
dict_code: 配置项编码
value: 配置值
label_cn: 配置项中文名称仅在配置不存在时需要
Returns:
是否设置成功
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True)
# 检查配置是否存在
cursor.execute(
"SELECT id FROM dict_data WHERE dict_type = %s AND dict_code = %s",
(cls.DICT_TYPE, dict_code)
)
existing = cursor.fetchone()
extension_attr = json.dumps({"value": value}, ensure_ascii=False)
if existing:
# 更新现有配置
update_query = """
UPDATE dict_data
SET extension_attr = %s, update_time = NOW()
WHERE dict_type = %s AND dict_code = %s
"""
cursor.execute(update_query, (extension_attr, cls.DICT_TYPE, dict_code))
else:
# 插入新配置
if not label_cn:
label_cn = dict_code
insert_query = """
INSERT INTO dict_data (
dict_type, dict_code, parent_code, label_cn,
extension_attr, status, sort_order
) VALUES (%s, %s, 'ROOT', %s, %s, 1, 0)
"""
cursor.execute(insert_query, (cls.DICT_TYPE, dict_code, label_cn, extension_attr))
conn.commit()
cursor.close()
return True
except Exception as e:
print(f"Error setting config {dict_code}: {e}")
return False
@classmethod
def get_all_configs(cls) -> Dict[str, Any]:
"""
获取所有系统配置
Returns:
配置字典 {dict_code: value}
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True)
query = """
SELECT dict_code, label_cn, extension_attr
FROM dict_data
WHERE dict_type = %s AND status = 1
ORDER BY sort_order
"""
cursor.execute(query, (cls.DICT_TYPE,))
results = cursor.fetchall()
cursor.close()
configs = {}
for row in results:
if row['extension_attr']:
try:
ext_attr = json.loads(row['extension_attr']) if isinstance(row['extension_attr'], str) else row['extension_attr']
configs[row['dict_code']] = ext_attr.get('value')
except (json.JSONDecodeError, AttributeError):
configs[row['dict_code']] = None
else:
configs[row['dict_code']] = None
return configs
except Exception as e:
print(f"Error getting all configs: {e}")
return {}
@classmethod
def batch_set_configs(cls, configs: Dict[str, Any]) -> bool:
"""
批量设置配置项
Args:
configs: 配置字典 {dict_code: value}
Returns:
是否全部设置成功
"""
success = True
for dict_code, value in configs.items():
if not cls.set_config(dict_code, value):
success = False
return success
# 便捷方法:获取特定配置
@classmethod
def get_asr_vocabulary_id(cls) -> Optional[str]:
"""获取ASR热词字典ID"""
return cls.get_config(cls.ASR_VOCABULARY_ID)
@classmethod
def get_voiceprint_template(cls, default: str = "") -> str:
"""获取声纹采集模版"""
return cls.get_config(cls.VOICEPRINT_TEMPLATE, default)
@classmethod
def get_timeline_pagesize(cls, default: int = 10) -> int:
"""获取会议时间轴每页数量"""
value = cls.get_config(cls.TIMELINE_PAGESIZE, str(default))
try:
return int(value)
except (ValueError, TypeError):
return default
@classmethod
def get_default_reset_password(cls, default: str = "111111") -> str:
"""获取默认重置密码"""
return cls.get_config(cls.DEFAULT_RESET_PASSWORD, default)
@classmethod
def get_max_audio_size(cls, default: int = 100) -> int:
"""获取上传音频文件大小限制MB"""
value = cls.get_config(cls.MAX_AUDIO_SIZE, str(default))
try:
return int(value)
except (ValueError, TypeError):
return default
# LLM模型配置获取方法
@classmethod
def get_llm_model_name(cls, default: str = "qwen-plus") -> str:
"""获取LLM模型名称"""
return cls.get_config(cls.LLM_MODEL_NAME, default)
@classmethod
def get_llm_timeout(cls, default: int = 120) -> int:
"""获取LLM超时时间"""
value = cls.get_config(cls.LLM_TIMEOUT, str(default))
try:
return int(value)
except (ValueError, TypeError):
return default
@classmethod
def get_llm_temperature(cls, default: float = 0.7) -> float:
"""获取LLM temperature参数"""
value = cls.get_config(cls.LLM_TEMPERATURE, str(default))
try:
return float(value)
except (ValueError, TypeError):
return default
@classmethod
def get_llm_top_p(cls, default: float = 0.9) -> float:
"""获取LLM top_p参数"""
value = cls.get_config(cls.LLM_TOP_P, str(default))
try:
return float(value)
except (ValueError, TypeError):
return default

View File

@ -17,7 +17,6 @@ class VoiceprintService:
def __init__(self): def __init__(self):
self.voiceprint_dir = config_module.VOICEPRINT_DIR self.voiceprint_dir = config_module.VOICEPRINT_DIR
self.voiceprint_config = config_module.VOICEPRINT_CONFIG
def get_user_voiceprint_status(self, user_id: int) -> Dict: def get_user_voiceprint_status(self, user_id: int) -> Dict:
""" """

View File

@ -1,56 +0,0 @@
import mysql.connector
# 连接数据库
conn = mysql.connector.connect(
host="10.100.51.51",
port=3306,
user="root",
password="Unis@123",
database="imeeting_dev"
)
cursor = conn.cursor()
print("=" * 80)
print("dict_data 表结构:")
print("=" * 80)
cursor.execute("SHOW CREATE TABLE dict_data")
result = cursor.fetchone()
print(result[1])
print("\n")
print("=" * 80)
print("dict_data 表数据:")
print("=" * 80)
cursor.execute("SELECT * FROM dict_data ORDER BY dict_code, sort_order")
rows = cursor.fetchall()
cursor.execute("SHOW COLUMNS FROM dict_data")
columns = [col[0] for col in cursor.fetchall()]
print(" | ".join(columns))
print("-" * 80)
for row in rows:
print(" | ".join(str(val) for val in row))
print("\n")
print("=" * 80)
print("client_download 表结构:")
print("=" * 80)
cursor.execute("SHOW CREATE TABLE client_download")
result = cursor.fetchone()
print(result[1])
print("\n")
print("=" * 80)
print("client_download 表数据:")
print("=" * 80)
cursor.execute("SELECT * FROM client_download")
rows = cursor.fetchall()
cursor.execute("SHOW COLUMNS FROM client_download")
columns = [col[0] for col in cursor.fetchall()]
print(" | ".join(columns))
print("-" * 80)
for row in rows:
print(" | ".join(str(val) for val in row))
cursor.close()
conn.close()

View File

@ -1,7 +0,0 @@
{
"model_name": "qwen-plus",
"template_text": "我正在进行声纹采集,这段语音将用于身份识别和验证。\n声纹技术能够识别每个人独特的声音特征用于人声识别应用。",
"DEFAULT_RESET_PASSWORD": "123456",
"MAX_FILE_SIZE": 209715200,
"TIMELINE_PAGESIZE": 10
}

View File

@ -0,0 +1,16 @@
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,24 +0,0 @@
"""
测试APK解析功能
"""
from app.utils.apk_parser import parse_apk_with_androguard
# 测试导入是否正常
try:
from androguard.core.apk import APK
print("✅ androguard 导入成功")
print(f" androguard 模块路径: {APK.__module__}")
except ImportError as e:
print(f"❌ androguard 导入失败: {e}")
exit(1)
# 如果有测试APK文件可以在这里测试解析
# apk_path = "path/to/your/test.apk"
# result = parse_apk_with_androguard(apk_path)
# print(f"解析结果: {result}")
print("\n📋 测试结果:")
print(" 1. androguard 库已成功安装")
print(" 2. 导入路径正确: androguard.core.apk.APK")
print(" 3. 可以正常使用 APK 解析功能")
print("\n💡 提示: 请重启后台服务以使更改生效")