增加了字段管理
parent
85d16a60da
commit
1a15cdff88
|
|
@ -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)}"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)}"
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -18,3 +18,6 @@ python-multipart
|
||||||
|
|
||||||
# System Monitoring
|
# System Monitoring
|
||||||
psutil
|
psutil
|
||||||
|
|
||||||
|
# APK Parsing
|
||||||
|
androguard
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
@ -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💡 提示: 请重启后台服务以使更改生效")
|
||||||
Loading…
Reference in New Issue