增加了字段管理

main
mula.liu 2025-12-18 19:58:38 +08:00
parent 85d16a60da
commit 1a15cdff88
15 changed files with 814 additions and 65 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
app.zip

Binary file not shown.

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form
from app.models.models import ( from app.models.models import (
ClientDownload, ClientDownload,
CreateClientDownloadRequest, CreateClientDownloadRequest,
@ -8,20 +8,30 @@ from app.models.models import (
from app.core.database import get_db_connection from app.core.database import get_db_connection
from app.core.auth import get_current_user, get_current_admin_user from app.core.auth import get_current_user, get_current_admin_user
from app.core.response import create_api_response from app.core.response import create_api_response
from app.core.config import CLIENT_DIR, ALLOWED_CLIENT_EXTENSIONS, MAX_CLIENT_SIZE, APP_CONFIG
from app.utils.apk_parser import parse_apk_with_androguard
from typing import Optional from typing import Optional
from pathlib import Path
import os
import shutil
router = APIRouter() router = APIRouter()
@router.get("/clients", response_model=dict) @router.get("/clients", response_model=dict)
async def get_client_downloads( async def get_client_downloads(
platform_type: Optional[str] = None, platform_code: Optional[str] = None,
platform_name: Optional[str] = None,
is_active: Optional[bool] = None, is_active: Optional[bool] = None,
page: int = 1, page: int = 1,
size: int = 50 size: int = 50
): ):
""" """
获取客户端下载列表公开接口所有用户可访问 获取客户端下载列表管理后台接口
参数
platform_code: 平台编码 WIN, MAC, ANDROID等
is_active: 是否启用
page: 页码
size: 每页数量
""" """
try: try:
with get_db_connection() as conn: with get_db_connection() as conn:
@ -31,13 +41,9 @@ async def get_client_downloads(
where_clauses = [] where_clauses = []
params = [] params = []
if platform_type: if platform_code:
where_clauses.append("platform_type = %s") where_clauses.append("platform_code = %s")
params.append(platform_type) params.append(platform_code)
if platform_name:
where_clauses.append("platform_name = %s")
params.append(platform_name)
if is_active is not None: if is_active is not None:
where_clauses.append("is_active = %s") where_clauses.append("is_active = %s")
@ -50,12 +56,12 @@ async def get_client_downloads(
cursor.execute(count_query, params) cursor.execute(count_query, params)
total = cursor.fetchone()['total'] total = cursor.fetchone()['total']
# 获取列表数据 # 获取列表数据 - 按 platform_code 和版本号排序
offset = (page - 1) * size offset = (page - 1) * size
list_query = f""" list_query = f"""
SELECT * FROM client_downloads SELECT * FROM client_downloads
WHERE {where_clause} WHERE {where_clause}
ORDER BY platform_type, platform_name, version_code DESC ORDER BY platform_code, version_code DESC
LIMIT %s OFFSET %s LIMIT %s OFFSET %s
""" """
cursor.execute(list_query, params + [size, offset]) cursor.execute(list_query, params + [size, offset])
@ -85,31 +91,48 @@ async def get_client_downloads(
async def get_latest_clients(): async def get_latest_clients():
""" """
获取所有平台的最新版本客户端公开接口用于首页下载 获取所有平台的最新版本客户端公开接口用于首页下载
返回按平台类型分组的最新客户端包含平台的中英文名称
""" """
try: try:
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(dictionary=True)
# 关联 dict_data 获取平台信息
query = """ query = """
SELECT * FROM client_downloads SELECT cd.*, dd.label_cn, dd.label_en, dd.parent_code, dd.extension_attr
WHERE is_active = TRUE AND is_latest = TRUE FROM client_downloads cd
ORDER BY platform_type, platform_name LEFT JOIN dict_data dd ON cd.platform_code = dd.dict_code
AND dd.dict_type = 'client_platform'
WHERE cd.is_active = TRUE AND cd.is_latest = TRUE
ORDER BY dd.parent_code, dd.sort_order, cd.platform_code
""" """
cursor.execute(query) cursor.execute(query)
clients = cursor.fetchall() clients = cursor.fetchall()
# 处理 JSON 字段
for client in clients:
if client.get('extension_attr'):
try:
import json
client['extension_attr'] = json.loads(client['extension_attr'])
except:
client['extension_attr'] = {}
cursor.close() cursor.close()
# 按平台类型分组 # 按 parent_code 分组
mobile_clients = [] mobile_clients = []
desktop_clients = [] desktop_clients = []
terminal_clients = [] terminal_clients = []
for client in clients: for client in clients:
if client['platform_type'] == 'mobile': parent_code = client.get('parent_code', '').upper()
if parent_code == 'MOBILE':
mobile_clients.append(client) mobile_clients.append(client)
elif client['platform_type'] == 'desktop': elif parent_code == 'DESKTOP':
desktop_clients.append(client) desktop_clients.append(client)
elif client['platform_type'] == 'terminal': elif parent_code == 'TERMINAL':
terminal_clients.append(client) terminal_clients.append(client)
return create_api_response( return create_api_response(
@ -130,39 +153,75 @@ async def get_latest_clients():
@router.get("/clients/latest/by-platform", response_model=dict) @router.get("/clients/latest/by-platform", response_model=dict)
async def get_latest_version_by_platform_type_and_name( async def get_latest_version_by_code(
platform_type: str, platform_type: Optional[str] = None,
platform_name: str platform_name: Optional[str] = None,
platform_code: Optional[str] = None
): ):
""" """
通过平台类型和平台名称获取最新版本公开接口用于客户端版本检查 获取最新版本客户端公开接口用于客户端版本检查
支持两种调用方式
1. 旧版方式 platform_type platform_name兼容已发布的终端
2. 新版方式 platform_code推荐使用
参数 参数
platform_type: 平台类型 (mobile, desktop, terminal) platform_type: 平台类型 (mobile, desktop, terminal) - 旧版参数
platform_name: 具体平台 (ios, android, windows, mac_intel, mac_m, linux, mcu) platform_name: 具体平台 (ios, android, windows等) - 旧版参数
platform_code: 平台编码 (WIN, MAC, LINUX, IOS, ANDROID等) - 新版参数
""" """
try: try:
with get_db_connection() as conn: with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True) cursor = conn.cursor(dictionary=True)
query = """ # 优先使用 platform_code新版
SELECT * FROM client_downloads if platform_code:
WHERE platform_type = %s query = """
AND platform_name = %s SELECT * FROM client_downloads
AND is_active = TRUE WHERE platform_code = %s
AND is_latest = TRUE AND is_active = TRUE
LIMIT 1 AND is_latest = TRUE
""" LIMIT 1
cursor.execute(query, (platform_type, platform_name)) """
client = cursor.fetchone() cursor.execute(query, (platform_code,))
cursor.close() client = cursor.fetchone()
if not client: if not client:
cursor.close()
return create_api_response(
code="404",
message=f"未找到平台编码 {platform_code} 的客户端"
)
# 使用 platform_type 和 platform_name旧版兼容
elif platform_type and platform_name:
query = """
SELECT * FROM client_downloads
WHERE platform_type = %s
AND platform_name = %s
AND is_active = TRUE
AND is_latest = TRUE
LIMIT 1
"""
cursor.execute(query, (platform_type, platform_name))
client = cursor.fetchone()
if not client:
cursor.close()
return create_api_response(
code="404",
message=f"未找到平台类型 {platform_type} 下的 {platform_name} 客户端"
)
else:
cursor.close()
return create_api_response( return create_api_response(
code="404", code="400",
message=f"未找到平台类型 {platform_type} 下的 {platform_name} 客户端" message="请提供 platform_code 参数"
) )
cursor.close()
return create_api_response( return create_api_response(
code="200", code="200",
message="获取成功", message="获取成功",
@ -216,6 +275,8 @@ async def create_client_download(
): ):
""" """
创建新的客户端版本仅管理员 创建新的客户端版本仅管理员
注意: platform_type platform_name 为兼容字段可不传
""" """
try: try:
with get_db_connection() as conn: with get_db_connection() as conn:
@ -226,21 +287,23 @@ async def create_client_download(
update_query = """ update_query = """
UPDATE client_downloads UPDATE client_downloads
SET is_latest = FALSE SET is_latest = FALSE
WHERE platform_name = %s WHERE platform_code = %s
""" """
cursor.execute(update_query, (request.platform_name,)) cursor.execute(update_query, (request.platform_code,))
# 插入新版本 # 插入新版本 - platform_type 和 platform_name 允许为 NULL
insert_query = """ insert_query = """
INSERT INTO client_downloads ( INSERT INTO client_downloads (
platform_type, platform_name, version, version_code, platform_type, platform_name, platform_code,
download_url, file_size, release_notes, is_active, version, version_code, download_url, file_size,
is_latest, min_system_version, created_by release_notes, is_active, is_latest, min_system_version,
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) created_by
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""" """
cursor.execute(insert_query, ( cursor.execute(insert_query, (
request.platform_type, request.platform_type, # 可为 None
request.platform_name, request.platform_name, # 可为 None
request.platform_code, # 必填
request.version, request.version,
request.version_code, request.version_code,
request.download_url, request.download_url,
@ -294,17 +357,31 @@ async def update_client_download(
# 如果设置为最新版本,先将同平台的其他版本设为非最新 # 如果设置为最新版本,先将同平台的其他版本设为非最新
if request.is_latest: if request.is_latest:
# 使用 platform_code (如果有更新) 或现有的 platform_code
platform_code = request.platform_code if request.platform_code else existing['platform_code']
update_query = """ update_query = """
UPDATE client_downloads UPDATE client_downloads
SET is_latest = FALSE SET is_latest = FALSE
WHERE platform_name = %s AND id != %s WHERE platform_code = %s AND id != %s
""" """
cursor.execute(update_query, (existing['platform_name'], id)) cursor.execute(update_query, (platform_code, id))
# 构建更新语句 # 构建更新语句
update_fields = [] update_fields = []
params = [] params = []
if request.platform_type is not None:
update_fields.append("platform_type = %s")
params.append(request.platform_type)
if request.platform_name is not None:
update_fields.append("platform_name = %s")
params.append(request.platform_name)
if request.platform_code is not None:
update_fields.append("platform_code = %s")
params.append(request.platform_code)
if request.version is not None: if request.version is not None:
update_fields.append("version = %s") update_fields.append("version = %s")
params.append(request.version) params.append(request.version)
@ -403,3 +480,86 @@ async def delete_client_download(
code="500", code="500",
message=f"删除客户端版本失败: {str(e)}" message=f"删除客户端版本失败: {str(e)}"
) )
@router.post("/clients/upload", response_model=dict)
async def upload_client_installer(
platform_code: str = Form(...),
file: UploadFile = File(...),
current_user: dict = Depends(get_current_admin_user)
):
"""
上传客户端安装包仅管理员
参数
platform_code: 平台编码 WIN, MAC, ANDROID等
file: 安装包文件
返回
文件信息包括文件大小下载URL以及APK的版本信息如果是APK
"""
try:
# 验证文件扩展名
file_ext = Path(file.filename).suffix.lower()
if file_ext not in ALLOWED_CLIENT_EXTENSIONS:
return create_api_response(
code="400",
message=f"不支持的文件类型: {file_ext}。支持的类型: {', '.join(ALLOWED_CLIENT_EXTENSIONS)}"
)
# 创建平台目录
platform_dir = CLIENT_DIR / platform_code
platform_dir.mkdir(parents=True, exist_ok=True)
# 生成文件名(保留原始文件名)
file_path = platform_dir / file.filename
# 检查文件大小
file.file.seek(0, 2) # 移动到文件末尾
file_size = file.file.tell() # 获取文件大小
file.file.seek(0) # 移回文件开头
if file_size > MAX_CLIENT_SIZE:
return create_api_response(
code="400",
message=f"文件过大,最大允许 {MAX_CLIENT_SIZE / 1024 / 1024} MB"
)
# 保存文件
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# 构建下载URL
base_url = APP_CONFIG['base_url'].rstrip('/')
download_url = f"{base_url}/uploads/clients/{platform_code}/{file.filename}"
# 准备返回数据
result = {
"file_name": file.filename,
"file_size": file_size,
"download_url": download_url,
"platform_code": platform_code
}
# 如果是APK文件尝试解析版本信息
if file_ext == '.apk':
apk_info = parse_apk_with_androguard(str(file_path))
if apk_info:
result['version_code'] = apk_info.get('version_code')
result['version_name'] = apk_info.get('version_name')
result['note'] = apk_info.get('note', '')
else:
# APK解析失败给出提示
result['note'] = 'APK解析失败请检查后台日志。您可以手动输入版本信息。'
return create_api_response(
code="200",
message="文件上传成功",
data=result
)
except Exception as e:
return create_api_response(
code="500",
message=f"文件上传失败: {str(e)}"
)

View File

@ -0,0 +1,413 @@
from fastapi import APIRouter, HTTPException, Depends
from app.core.database import get_db_connection
from app.core.auth import get_current_user, get_current_admin_user
from app.core.response import create_api_response
from pydantic import BaseModel
from typing import Optional, List
import json
router = APIRouter()
class DictDataItem(BaseModel):
"""码表数据项"""
id: int
dict_type: str
dict_code: str
parent_code: str
tree_path: Optional[str] = None
label_cn: str
label_en: Optional[str] = None
sort_order: int
extension_attr: Optional[dict] = None
is_default: int
status: int
create_time: str
class CreateDictDataRequest(BaseModel):
"""创建码表数据请求"""
dict_type: str = "client_platform"
dict_code: str
parent_code: str = "ROOT"
label_cn: str
label_en: Optional[str] = None
sort_order: int = 0
extension_attr: Optional[dict] = None
is_default: int = 0
status: int = 1
class UpdateDictDataRequest(BaseModel):
"""更新码表数据请求"""
parent_code: Optional[str] = None
label_cn: Optional[str] = None
label_en: Optional[str] = None
sort_order: Optional[int] = None
extension_attr: Optional[dict] = None
is_default: Optional[int] = None
status: Optional[int] = None
@router.get("/dict/types", response_model=dict)
async def get_dict_types():
"""
获取所有字典类型公开接口
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True)
query = """
SELECT DISTINCT dict_type
FROM dict_data
WHERE status = 1
ORDER BY dict_type
"""
cursor.execute(query)
types = cursor.fetchall()
cursor.close()
return create_api_response(
code="200",
message="获取成功",
data={"types": [t['dict_type'] for t in types]}
)
except Exception as e:
return create_api_response(
code="500",
message=f"获取字典类型失败: {str(e)}"
)
@router.get("/dict/{dict_type}", response_model=dict)
async def get_dict_data_by_type(dict_type: str):
"""
获取指定类型的所有码表数据公开接口
支持树形结构
参数
dict_type: 字典类型 'client_platform'
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True)
query = """
SELECT id, dict_type, dict_code, parent_code, tree_path,
label_cn, label_en, sort_order, extension_attr,
is_default, status, create_time
FROM dict_data
WHERE dict_type = %s AND status = 1
ORDER BY parent_code, sort_order, dict_code
"""
cursor.execute(query, (dict_type,))
items = cursor.fetchall()
cursor.close()
# 处理JSON字段
for item in items:
if item.get('extension_attr'):
try:
item['extension_attr'] = json.loads(item['extension_attr'])
except:
item['extension_attr'] = {}
# 构建树形结构
tree_data = []
nodes_map = {}
# 第一遍:创建所有节点
for item in items:
nodes_map[item['dict_code']] = {
**item,
'children': []
}
# 第二遍:构建树形关系
for item in items:
node = nodes_map[item['dict_code']]
parent_code = item['parent_code']
if parent_code == 'ROOT':
tree_data.append(node)
elif parent_code in nodes_map:
nodes_map[parent_code]['children'].append(node)
return create_api_response(
code="200",
message="获取成功",
data={
"items": items, # 平铺数据
"tree": tree_data # 树形数据
}
)
except Exception as e:
return create_api_response(
code="500",
message=f"获取码表数据失败: {str(e)}"
)
@router.get("/dict/{dict_type}/{dict_code}", response_model=dict)
async def get_dict_data_by_code(dict_type: str, dict_code: str):
"""
获取指定编码的码表数据公开接口
参数
dict_type: 字典类型
dict_code: 字典编码
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True)
query = """
SELECT id, dict_type, dict_code, parent_code, tree_path,
label_cn, label_en, sort_order, extension_attr,
is_default, status, create_time
FROM dict_data
WHERE dict_type = %s AND dict_code = %s
LIMIT 1
"""
cursor.execute(query, (dict_type, dict_code))
item = cursor.fetchone()
cursor.close()
if not item:
return create_api_response(
code="404",
message=f"未找到编码 {dict_code} 的数据"
)
# 处理JSON字段
if item.get('extension_attr'):
try:
item['extension_attr'] = json.loads(item['extension_attr'])
except:
item['extension_attr'] = {}
return create_api_response(
code="200",
message="获取成功",
data=item
)
except Exception as e:
return create_api_response(
code="500",
message=f"获取码表数据失败: {str(e)}"
)
@router.post("/dict", response_model=dict)
async def create_dict_data(
request: CreateDictDataRequest,
current_user: dict = Depends(get_current_admin_user)
):
"""
创建码表数据仅管理员
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor()
# 检查是否已存在
cursor.execute(
"SELECT id FROM dict_data WHERE dict_type = %s AND dict_code = %s",
(request.dict_type, request.dict_code)
)
if cursor.fetchone():
cursor.close()
return create_api_response(
code="400",
message=f"编码 {request.dict_code} 已存在"
)
# 插入数据
query = """
INSERT INTO dict_data (
dict_type, dict_code, parent_code, label_cn, label_en,
sort_order, extension_attr, is_default, status
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
extension_json = json.dumps(request.extension_attr) if request.extension_attr else None
cursor.execute(query, (
request.dict_type,
request.dict_code,
request.parent_code,
request.label_cn,
request.label_en,
request.sort_order,
extension_json,
request.is_default,
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("/dict/{id}", response_model=dict)
async def update_dict_data(
id: int,
request: UpdateDictDataRequest,
current_user: dict = Depends(get_current_admin_user)
):
"""
更新码表数据仅管理员
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True)
# 检查是否存在
cursor.execute("SELECT * FROM dict_data WHERE id = %s", (id,))
existing = cursor.fetchone()
if not existing:
cursor.close()
return create_api_response(
code="404",
message="码表数据不存在"
)
# 构建更新语句
update_fields = []
params = []
if request.parent_code is not None:
update_fields.append("parent_code = %s")
params.append(request.parent_code)
if request.label_cn is not None:
update_fields.append("label_cn = %s")
params.append(request.label_cn)
if request.label_en is not None:
update_fields.append("label_en = %s")
params.append(request.label_en)
if request.sort_order is not None:
update_fields.append("sort_order = %s")
params.append(request.sort_order)
if request.extension_attr is not None:
update_fields.append("extension_attr = %s")
params.append(json.dumps(request.extension_attr))
if request.is_default is not None:
update_fields.append("is_default = %s")
params.append(request.is_default)
if request.status is not None:
update_fields.append("status = %s")
params.append(request.status)
if not update_fields:
cursor.close()
return create_api_response(
code="400",
message="没有要更新的字段"
)
# 执行更新
update_query = f"""
UPDATE dict_data
SET {', '.join(update_fields)}
WHERE id = %s
"""
params.append(id)
cursor.execute(update_query, 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("/dict/{id}", response_model=dict)
async def delete_dict_data(
id: int,
current_user: dict = Depends(get_current_admin_user)
):
"""
删除码表数据仅管理员
注意如果有子节点或被引用应该拒绝删除
"""
try:
with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True)
# 检查是否存在
cursor.execute("SELECT dict_code FROM dict_data WHERE id = %s", (id,))
existing = cursor.fetchone()
if not existing:
cursor.close()
return create_api_response(
code="404",
message="码表数据不存在"
)
# 检查是否有子节点
cursor.execute(
"SELECT COUNT(*) as count FROM dict_data WHERE parent_code = %s",
(existing['dict_code'],)
)
if cursor.fetchone()['count'] > 0:
cursor.close()
return create_api_response(
code="400",
message="该节点存在子节点,无法删除"
)
# 检查是否被client_downloads引用
cursor.execute(
"SELECT COUNT(*) as count FROM client_downloads WHERE platform_code = %s",
(existing['dict_code'],)
)
if cursor.fetchone()['count'] > 0:
cursor.close()
return create_api_response(
code="400",
message="该平台编码已被客户端下载记录引用,无法删除"
)
# 执行删除
cursor.execute("DELETE FROM dict_data 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)}"
)

View File

@ -113,8 +113,8 @@ def get_meetings(
# 构建基础查询 # 构建基础查询
base_query = ''' base_query = '''
SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags, SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags, m.access_password,
m.user_id as creator_id, u.caption as creator_username, af.file_path as audio_file_path m.user_id as creator_id, u.caption as creator_username, MAX(af.file_path) as audio_file_path
FROM meetings m FROM meetings m
JOIN users u ON m.user_id = u.user_id JOIN users u ON m.user_id = u.user_id
LEFT JOIN audio_files af ON m.meeting_id = af.meeting_id LEFT JOIN audio_files af ON m.meeting_id = af.meeting_id
@ -139,9 +139,8 @@ def get_meetings(
cursor.execute(count_query, params) cursor.execute(count_query, params)
total = cursor.fetchone()['total'] total = cursor.fetchone()['total']
# 添加GROUP BY如果联表了attendees # 添加GROUP BY因为使用了MAX聚合函数总是需要GROUP BY
if has_attendees_join: base_query += " GROUP BY m.meeting_id"
base_query += " GROUP BY m.meeting_id"
# 计算分页 # 计算分页
total_pages = (total + page_size - 1) // page_size total_pages = (total + page_size - 1) // page_size

View File

@ -9,25 +9,29 @@ AUDIO_DIR = UPLOAD_DIR / "audio"
TEMP_UPLOAD_DIR = UPLOAD_DIR / "temp_audio" TEMP_UPLOAD_DIR = UPLOAD_DIR / "temp_audio"
MARKDOWN_DIR = UPLOAD_DIR / "markdown" MARKDOWN_DIR = UPLOAD_DIR / "markdown"
VOICEPRINT_DIR = UPLOAD_DIR / "voiceprint" VOICEPRINT_DIR = UPLOAD_DIR / "voiceprint"
CLIENT_DIR = UPLOAD_DIR / "clients"
# 文件上传配置 # 文件上传配置
ALLOWED_EXTENSIONS = {".mp3", ".wav", ".m4a", ".mpeg", ".mp4"} ALLOWED_EXTENSIONS = {".mp3", ".wav", ".m4a", ".mpeg", ".mp4"}
ALLOWED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"} ALLOWED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
ALLOWED_VOICEPRINT_EXTENSIONS = {".wav"} ALLOWED_VOICEPRINT_EXTENSIONS = {".wav"}
ALLOWED_CLIENT_EXTENSIONS = {".apk", ".exe", ".dmg", ".deb", ".rpm", ".pkg", ".msi", ".zip", ".tar.gz"}
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10MB MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10MB
MAX_CLIENT_SIZE = 500 * 1024 * 1024 # 500MB for client installers
# 确保上传目录存在 # 确保上传目录存在
UPLOAD_DIR.mkdir(exist_ok=True) UPLOAD_DIR.mkdir(exist_ok=True)
AUDIO_DIR.mkdir(exist_ok=True) AUDIO_DIR.mkdir(exist_ok=True)
MARKDOWN_DIR.mkdir(exist_ok=True) MARKDOWN_DIR.mkdir(exist_ok=True)
VOICEPRINT_DIR.mkdir(exist_ok=True) VOICEPRINT_DIR.mkdir(exist_ok=True)
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_dev'), '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'
@ -52,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', None), 'password': os.getenv('REDIS_PASSWORD', 'Unis@123'),
'decode_responses': True 'decode_responses': True
} }

View File

@ -13,7 +13,7 @@ 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 from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data
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 from app.api.endpoints.admin import load_system_config
@ -50,6 +50,7 @@ app.include_router(tasks.router, prefix="/api", tags=["Tasks"])
app.include_router(prompts.router, prefix="/api", tags=["Prompts"]) app.include_router(prompts.router, prefix="/api", tags=["Prompts"])
app.include_router(knowledge_base.router, prefix="/api", tags=["KnowledgeBase"]) app.include_router(knowledge_base.router, prefix="/api", tags=["KnowledgeBase"])
app.include_router(client_downloads.router, prefix="/api", tags=["ClientDownloads"]) app.include_router(client_downloads.router, prefix="/api", tags=["ClientDownloads"])
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"])

View File

@ -167,8 +167,9 @@ class KnowledgeBaseListResponse(BaseModel):
# 客户端下载相关模型 # 客户端下载相关模型
class ClientDownload(BaseModel): class ClientDownload(BaseModel):
id: int id: int
platform_type: str # 'mobile' or 'desktop' platform_type: Optional[str] = None # 兼容旧版:'mobile', 'desktop', 'terminal'
platform_name: str # 'ios', 'android', 'windows', 'mac_intel', 'mac_m', 'linux' platform_name: Optional[str] = None # 兼容旧版:'ios', 'android', 'windows', 'mac_intel', 'mac_m', 'linux'
platform_code: str # 新版平台编码,关联 dict_data.dict_code
version: str version: str
version_code: int version_code: int
download_url: str download_url: str
@ -182,8 +183,9 @@ class ClientDownload(BaseModel):
created_by: Optional[int] = None created_by: Optional[int] = None
class CreateClientDownloadRequest(BaseModel): class CreateClientDownloadRequest(BaseModel):
platform_type: str platform_type: Optional[str] = None # 兼容旧版
platform_name: str platform_name: Optional[str] = None # 兼容旧版
platform_code: str # 必填,关联 dict_data
version: str version: str
version_code: int version_code: int
download_url: str download_url: str
@ -194,6 +196,9 @@ class CreateClientDownloadRequest(BaseModel):
min_system_version: Optional[str] = None min_system_version: Optional[str] = None
class UpdateClientDownloadRequest(BaseModel): class UpdateClientDownloadRequest(BaseModel):
platform_type: Optional[str] = None
platform_name: Optional[str] = None
platform_code: Optional[str] = None
version: Optional[str] = None version: Optional[str] = None
version_code: Optional[int] = None version_code: Optional[int] = None
download_url: Optional[str] = None download_url: Optional[str] = None

View File

@ -0,0 +1,36 @@
"""
APK解析工具
用于从APK文件中提取版本信息
"""
import zipfile
import xml.etree.ElementTree as ET
import struct
# 如果安装了 androguard使用更可靠的解析方法
def parse_apk_with_androguard(apk_path):
"""
使用 androguard 库解析 APK
需要先安装: pip install androguard
"""
try:
from androguard.core.apk import APK
apk = APK(apk_path)
version_code = apk.get_androidversion_code()
version_name = apk.get_androidversion_name()
print(f"APK解析成功: version_code={version_code}, version_name={version_name}")
return {
'version_code': int(version_code) if version_code else None,
'version_name': version_name
}
except ImportError as ie:
print(f"androguard 导入失败: {str(ie)}")
return parse_apk(apk_path)
except Exception as e:
print(f"使用androguard解析APK失败: {str(e)}")
import traceback
traceback.print_exc()
return None

View File

@ -0,0 +1,56 @@
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

@ -18,3 +18,6 @@ python-multipart
# System Monitoring # System Monitoring
psutil psutil
# APK Parsing
androguard

View File

@ -10,3 +10,4 @@ dashscope
PyJWT>=2.8.0 PyJWT>=2.8.0
python-jose[cryptography]>=3.3.0 python-jose[cryptography]>=3.3.0
psutil psutil
androguard

View File

@ -0,0 +1,10 @@
-- 添加 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

@ -0,0 +1,37 @@
-- 客户端下载管理表
-- 保留 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);

24
test_apk_parser.py 100644
View File

@ -0,0 +1,24 @@
"""
测试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💡 提示: 请重启后台服务以使更改生效")