diff --git a/.gemini-clipboard/clipboard-1768468287858.png b/.gemini-clipboard/clipboard-1768468287858.png
deleted file mode 100644
index bcd8f1a..0000000
Binary files a/.gemini-clipboard/clipboard-1768468287858.png and /dev/null differ
diff --git a/.gemini-clipboard/clipboard-1768470582901.png b/.gemini-clipboard/clipboard-1768470582901.png
deleted file mode 100644
index 7b998d4..0000000
Binary files a/.gemini-clipboard/clipboard-1768470582901.png and /dev/null differ
diff --git a/.gemini-clipboard/clipboard-1768470643790.png b/.gemini-clipboard/clipboard-1768470643790.png
deleted file mode 100644
index cb6ed0c..0000000
Binary files a/.gemini-clipboard/clipboard-1768470643790.png and /dev/null differ
diff --git a/.gemini-clipboard/clipboard-1768974204648.png b/.gemini-clipboard/clipboard-1768974204648.png
new file mode 100644
index 0000000..6838957
Binary files /dev/null and b/.gemini-clipboard/clipboard-1768974204648.png differ
diff --git a/backend/app/api/endpoints/dict_data.py b/backend/app/api/endpoints/dict_data.py
index b5c03c7..bc27185 100644
--- a/backend/app/api/endpoints/dict_data.py
+++ b/backend/app/api/endpoints/dict_data.py
@@ -82,10 +82,11 @@ async def get_dict_types():
@router.get("/dict/{dict_type}", response_model=dict)
-async def get_dict_data_by_type(dict_type: str):
+async def get_dict_data_by_type(dict_type: str, parent_code: Optional[str] = None):
"""
获取指定类型的所有码表数据(公开接口)
支持树形结构
+ 可选参数:parent_code 筛选特定父节点的子项(此时不返回树结构,只返回平铺列表)
参数:
dict_type: 字典类型,如 'client_platform'
@@ -100,9 +101,16 @@ async def get_dict_data_by_type(dict_type: str):
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,))
+ params = [dict_type]
+
+ if parent_code:
+ query += " AND parent_code = %s"
+ params.append(parent_code)
+
+ query += " ORDER BY parent_code, sort_order, dict_code"
+
+ cursor.execute(query, params)
items = cursor.fetchall()
cursor.close()
@@ -114,6 +122,17 @@ async def get_dict_data_by_type(dict_type: str):
except:
item['extension_attr'] = {}
+ # 如果指定了parent_code,只返回平铺列表
+ if parent_code:
+ return create_api_response(
+ code="200",
+ message="获取成功",
+ data={
+ "items": items,
+ "tree": []
+ }
+ )
+
# 构建树形结构
tree_data = []
nodes_map = {}
@@ -128,12 +147,12 @@ async def get_dict_data_by_type(dict_type: str):
# 第二遍:构建树形关系
for item in items:
node = nodes_map[item['dict_code']]
- parent_code = item['parent_code']
+ parent_code_val = item['parent_code']
- if parent_code == 'ROOT':
+ if parent_code_val == 'ROOT':
tree_data.append(node)
- elif parent_code in nodes_map:
- nodes_map[parent_code]['children'].append(node)
+ elif parent_code_val in nodes_map:
+ nodes_map[parent_code_val]['children'].append(node)
return create_api_response(
code="200",
diff --git a/backend/app/api/endpoints/external_apps.py b/backend/app/api/endpoints/external_apps.py
index 7ca3887..259307a 100644
--- a/backend/app/api/endpoints/external_apps.py
+++ b/backend/app/api/endpoints/external_apps.py
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException
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_optional_current_user, get_current_admin_user
from app.core.response import create_api_response
from app.core.config import BASE_DIR, EXTERNAL_APPS_DIR, ALLOWED_IMAGE_EXTENSIONS, MAX_IMAGE_SIZE
from app.utils.apk_parser import parse_apk_with_androguard
@@ -44,7 +44,7 @@ class UpdateExternalAppRequest(BaseModel):
async def get_external_apps(
app_type: Optional[str] = None,
is_active: Optional[bool] = None,
- current_user: dict = Depends(get_current_user)
+ current_user: dict = Depends(get_current_admin_user)
):
"""
获取外部应用列表(管理后台接口)
@@ -107,10 +107,19 @@ async def get_external_apps(
@router.get("/external-apps/active", response_model=dict)
-async def get_active_external_apps():
+async def get_active_external_apps(current_user: Optional[dict] = Depends(get_optional_current_user)):
"""
获取所有启用的外部应用(公开接口,供客户端调用)
+ 未登录返回空列表
"""
+ # 如果未登录,返回空列表
+ if not current_user:
+ return create_api_response(
+ code="200",
+ message="未登录",
+ data=[]
+ )
+
try:
with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True)
@@ -134,18 +143,10 @@ async def get_active_external_apps():
cursor.close()
- # 按类型分组
- native_apps = [app for app in apps if app['app_type'] == 'native']
- web_apps = [app for app in apps if app['app_type'] == 'web']
-
return create_api_response(
code="200",
message="获取成功",
- data={
- "native": native_apps,
- "web": web_apps,
- "all": apps
- }
+ data=apps
)
except Exception as e:
diff --git a/backend/app/api/endpoints/terminals.py b/backend/app/api/endpoints/terminals.py
new file mode 100644
index 0000000..9567fa6
--- /dev/null
+++ b/backend/app/api/endpoints/terminals.py
@@ -0,0 +1,166 @@
+from fastapi import APIRouter, Depends, HTTPException, Query
+from typing import Optional
+import traceback
+from app.core.auth import get_current_admin_user
+from app.core.response import create_api_response
+from app.models.models import CreateTerminalRequest, UpdateTerminalRequest
+from app.services.terminal_service import terminal_service
+
+router = APIRouter()
+
+@router.get("/terminals", response_model=dict)
+async def get_terminals(
+ page: int = Query(1, ge=1),
+ size: int = Query(20, ge=1, le=10000),
+ keyword: Optional[str] = None,
+ terminal_type: Optional[str] = None,
+ status: Optional[int] = None,
+ current_user: dict = Depends(get_current_admin_user)
+):
+ """
+ 获取终端设备列表(分页)
+ """
+ try:
+ terminals, total = terminal_service.get_terminals(
+ page=page,
+ size=size,
+ keyword=keyword,
+ terminal_type=terminal_type,
+ status=status
+ )
+ return create_api_response(
+ code="200",
+ message="获取成功",
+ data={
+ "items": terminals,
+ "total": total,
+ "page": page,
+ "size": size
+ }
+ )
+ except Exception as e:
+ traceback.print_exc()
+ return create_api_response(
+ code="500",
+ message=f"获取终端列表失败: {str(e)}"
+ )
+
+@router.post("/terminals", response_model=dict)
+async def create_terminal(
+ request: CreateTerminalRequest,
+ current_user: dict = Depends(get_current_admin_user)
+):
+ """
+ 创建新终端设备
+ """
+ try:
+ # 检查IMEI是否存在
+ existing = terminal_service.get_terminal_by_imei(request.imei)
+ if existing:
+ return create_api_response(code="400", message=f"IMEI {request.imei} 已存在")
+
+ terminal_id = terminal_service.create_terminal(request, current_user['user_id'])
+ return create_api_response(
+ code="200",
+ message="创建成功",
+ data={"id": terminal_id}
+ )
+ except Exception as e:
+ return create_api_response(
+ code="500",
+ message=f"创建终端失败: {str(e)}"
+ )
+
+@router.get("/terminals/{terminal_id}", response_model=dict)
+async def get_terminal_detail(
+ terminal_id: int,
+ current_user: dict = Depends(get_current_admin_user)
+):
+ """
+ 获取终端设备详情
+ """
+ try:
+ terminal = terminal_service.get_terminal_by_id(terminal_id)
+ if not terminal:
+ return create_api_response(code="404", message="终端不存在")
+
+ return create_api_response(
+ code="200",
+ message="获取成功",
+ data=terminal
+ )
+ except Exception as e:
+ return create_api_response(
+ code="500",
+ message=f"获取终端详情失败: {str(e)}"
+ )
+
+@router.put("/terminals/{terminal_id}", response_model=dict)
+async def update_terminal(
+ terminal_id: int,
+ request: UpdateTerminalRequest,
+ current_user: dict = Depends(get_current_admin_user)
+):
+ """
+ 更新终端设备信息
+ """
+ try:
+ # 检查是否存在
+ existing = terminal_service.get_terminal_by_id(terminal_id)
+ if not existing:
+ return create_api_response(code="404", message="终端不存在")
+
+ success = terminal_service.update_terminal(terminal_id, request)
+ if success:
+ return create_api_response(code="200", message="更新成功")
+ else:
+ return create_api_response(code="400", message="没有需要更新的字段")
+ except Exception as e:
+ return create_api_response(
+ code="500",
+ message=f"更新终端失败: {str(e)}"
+ )
+
+@router.delete("/terminals/{terminal_id}", response_model=dict)
+async def delete_terminal(
+ terminal_id: int,
+ current_user: dict = Depends(get_current_admin_user)
+):
+ """
+ 删除终端设备
+ """
+ try:
+ success = terminal_service.delete_terminal(terminal_id)
+ if success:
+ return create_api_response(code="200", message="删除成功")
+ else:
+ return create_api_response(code="404", message="终端不存在")
+ except Exception as e:
+ return create_api_response(
+ code="500",
+ message=f"删除终端失败: {str(e)}"
+ )
+
+@router.post("/terminals/{terminal_id}/status", response_model=dict)
+async def toggle_terminal_status(
+ terminal_id: int,
+ status: int = Query(..., description="1:启用, 0:停用"),
+ current_user: dict = Depends(get_current_admin_user)
+):
+ """
+ 切换终端启用/停用状态
+ """
+ try:
+ if status not in [0, 1]:
+ return create_api_response(code="400", message="状态值无效")
+
+ success = terminal_service.set_terminal_status(terminal_id, status)
+ if success:
+ return create_api_response(code="200", message="状态更新成功")
+ else:
+ return create_api_response(code="404", message="终端不存在")
+ except Exception as e:
+ return create_api_response(
+ code="500",
+ message=f"状态更新失败: {str(e)}"
+ )
diff --git a/backend/app/main.py b/backend/app/main.py
index b53514c..6057481 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -14,7 +14,7 @@ from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.openapi.docs import get_swagger_ui_html
-from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data, hot_words, external_apps
+from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data, hot_words, external_apps, terminals
from app.core.config import UPLOAD_DIR, API_CONFIG
app = FastAPI(
@@ -54,6 +54,7 @@ app.include_router(dict_data.router, prefix="/api", tags=["DictData"])
app.include_router(voiceprint.router, prefix="/api", tags=["Voiceprint"])
app.include_router(audio.router, prefix="/api", tags=["Audio"])
app.include_router(hot_words.router, prefix="/api", tags=["HotWords"])
+app.include_router(terminals.router, prefix="/api", tags=["Terminals"])
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
diff --git a/backend/app/models/models.py b/backend/app/models/models.py
index 647f8e7..f2f4259 100644
--- a/backend/app/models/models.py
+++ b/backend/app/models/models.py
@@ -274,3 +274,38 @@ class RolePermissionInfo(BaseModel):
class UpdateRolePermissionsRequest(BaseModel):
menu_ids: List[int]
+
+# 专用终端设备模型
+class Terminal(BaseModel):
+ id: int
+ imei: str
+ terminal_name: Optional[str] = None
+ terminal_type: str
+ terminal_type_name: Optional[str] = None # 终端类型名称(从字典获取)
+ description: Optional[str] = None
+ status: int # 1: 启用, 0: 停用
+ is_activated: int # 1: 已激活, 0: 未激活
+ activated_at: Optional[datetime.datetime] = None
+ firmware_version: Optional[str] = None
+ last_online_at: Optional[datetime.datetime] = None
+ ip_address: Optional[str] = None
+ mac_address: Optional[str] = None
+ created_at: datetime.datetime
+ updated_at: datetime.datetime
+ created_by: Optional[int] = None
+ creator_username: Optional[str] = None
+
+class CreateTerminalRequest(BaseModel):
+ imei: str
+ terminal_name: Optional[str] = None
+ terminal_type: str
+ description: Optional[str] = None
+ status: int = 1
+
+class UpdateTerminalRequest(BaseModel):
+ terminal_name: Optional[str] = None
+ terminal_type: Optional[str] = None
+ description: Optional[str] = None
+ status: Optional[int] = None
+ firmware_version: Optional[str] = None
+ mac_address: Optional[str] = None
diff --git a/backend/app/services/terminal_service.py b/backend/app/services/terminal_service.py
new file mode 100644
index 0000000..00656be
--- /dev/null
+++ b/backend/app/services/terminal_service.py
@@ -0,0 +1,191 @@
+from typing import List, Optional, Tuple, Dict, Any
+from app.core.database import get_db_connection
+from app.models.models import Terminal, CreateTerminalRequest, UpdateTerminalRequest
+import datetime
+
+class TerminalService:
+ def get_terminals(self,
+ page: int = 1,
+ size: int = 20,
+ keyword: Optional[str] = None,
+ terminal_type: Optional[str] = None,
+ status: Optional[int] = None) -> Tuple[List[Dict[str, Any]], int]:
+ """
+ 获取终端列表,支持分页和筛选
+ """
+ with get_db_connection() as conn:
+ cursor = conn.cursor(dictionary=True)
+
+ where_clauses = []
+ params = []
+
+ if keyword:
+ where_clauses.append("(t.imei LIKE %s OR t.terminal_name LIKE %s)")
+ keyword_param = f"%{keyword}%"
+ params.extend([keyword_param, keyword_param])
+
+ if terminal_type:
+ where_clauses.append("t.terminal_type = %s")
+ params.append(terminal_type)
+
+ if status is not None:
+ where_clauses.append("t.status = %s")
+ params.append(status)
+
+ where_clause = " AND ".join(where_clauses) if where_clauses else "1=1"
+
+ # 计算总数
+ count_query = f"SELECT COUNT(*) as total FROM terminals t WHERE {where_clause}"
+ cursor.execute(count_query, params)
+ total = cursor.fetchone()['total']
+
+ # 查询列表
+ offset = (page - 1) * size
+ list_query = f"""
+ SELECT
+ t.*,
+ u.username as creator_username,
+ dd.label_cn as terminal_type_name
+ FROM terminals t
+ LEFT JOIN users u ON t.created_by = u.user_id
+ LEFT JOIN dict_data dd ON t.terminal_type = dd.dict_code AND dd.dict_type = 'terminal_type'
+ WHERE {where_clause}
+ ORDER BY t.created_at DESC
+ LIMIT %s OFFSET %s
+ """
+ cursor.execute(list_query, params + [size, offset])
+ terminals = cursor.fetchall()
+
+ cursor.close()
+ return terminals, total
+
+ def get_terminal_by_id(self, terminal_id: int) -> Optional[Dict[str, Any]]:
+ """
+ 根据ID获取终端详情
+ """
+ with get_db_connection() as conn:
+ cursor = conn.cursor(dictionary=True)
+
+ query = """
+ SELECT
+ t.*,
+ u.username as creator_username,
+ dd.label_cn as terminal_type_name
+ FROM terminals t
+ LEFT JOIN users u ON t.created_by = u.user_id
+ LEFT JOIN dict_data dd ON t.terminal_type = dd.dict_code AND dd.dict_type = 'terminal_type'
+ WHERE t.id = %s
+ """
+ cursor.execute(query, (terminal_id,))
+ terminal = cursor.fetchone()
+
+ cursor.close()
+ return terminal
+
+ def get_terminal_by_imei(self, imei: str) -> Optional[Dict[str, Any]]:
+ """
+ 根据IMEI获取终端详情
+ """
+ with get_db_connection() as conn:
+ cursor = conn.cursor(dictionary=True)
+ cursor.execute("SELECT * FROM terminals WHERE imei = %s", (imei,))
+ terminal = cursor.fetchone()
+ cursor.close()
+ return terminal
+
+ def create_terminal(self, terminal_data: CreateTerminalRequest, user_id: int) -> int:
+ """
+ 创建新终端
+ """
+ with get_db_connection() as conn:
+ cursor = conn.cursor()
+
+ query = """
+ INSERT INTO terminals (
+ imei, terminal_name, terminal_type, description, status, created_by
+ ) VALUES (%s, %s, %s, %s, %s, %s)
+ """
+ cursor.execute(query, (
+ terminal_data.imei,
+ terminal_data.terminal_name,
+ terminal_data.terminal_type,
+ terminal_data.description,
+ terminal_data.status,
+ user_id
+ ))
+
+ new_id = cursor.lastrowid
+ conn.commit()
+ cursor.close()
+ return new_id
+
+ def update_terminal(self, terminal_id: int, terminal_data: UpdateTerminalRequest) -> bool:
+ """
+ 更新终端信息
+ """
+ with get_db_connection() as conn:
+ cursor = conn.cursor()
+
+ update_fields = []
+ params = []
+
+ if terminal_data.terminal_name is not None:
+ update_fields.append("terminal_name = %s")
+ params.append(terminal_data.terminal_name)
+
+ if terminal_data.terminal_type is not None:
+ update_fields.append("terminal_type = %s")
+ params.append(terminal_data.terminal_type)
+
+ if terminal_data.description is not None:
+ update_fields.append("description = %s")
+ params.append(terminal_data.description)
+
+ if terminal_data.status is not None:
+ update_fields.append("status = %s")
+ params.append(terminal_data.status)
+
+ if terminal_data.firmware_version is not None:
+ update_fields.append("firmware_version = %s")
+ params.append(terminal_data.firmware_version)
+
+ if terminal_data.mac_address is not None:
+ update_fields.append("mac_address = %s")
+ params.append(terminal_data.mac_address)
+
+ if not update_fields:
+ return False
+
+ query = f"UPDATE terminals SET {', '.join(update_fields)} WHERE id = %s"
+ params.append(terminal_id)
+
+ cursor.execute(query, params)
+ conn.commit()
+ cursor.close()
+ return True
+
+ def delete_terminal(self, terminal_id: int) -> bool:
+ """
+ 删除终端
+ """
+ with get_db_connection() as conn:
+ cursor = conn.cursor()
+ cursor.execute("DELETE FROM terminals WHERE id = %s", (terminal_id,))
+ deleted = cursor.rowcount > 0
+ conn.commit()
+ cursor.close()
+ return deleted
+
+ def set_terminal_status(self, terminal_id: int, status: int) -> bool:
+ """
+ 设置终端启用/停用状态
+ """
+ with get_db_connection() as conn:
+ cursor = conn.cursor()
+ cursor.execute("UPDATE terminals SET status = %s WHERE id = %s", (status, terminal_id))
+ updated = cursor.rowcount > 0
+ conn.commit()
+ cursor.close()
+ return updated
+
+terminal_service = TerminalService()
diff --git a/backend/sql/add_dedicated_terminal.sql b/backend/sql/add_dedicated_terminal.sql
index e0140ad..62abc5a 100644
--- a/backend/sql/add_dedicated_terminal.sql
+++ b/backend/sql/add_dedicated_terminal.sql
@@ -1,59 +1,29 @@
--- 添加专用终端类型支持
--- 修改 platform_type 枚举,添加 'terminal' 类型
-
-ALTER TABLE client_downloads
-MODIFY COLUMN platform_type ENUM('mobile', 'desktop', 'terminal') NOT NULL
-COMMENT '平台类型:mobile-移动端, desktop-桌面端, terminal-专用终端';
-
--- 插入专用终端示例数据
-
--- Android 专用终端
-INSERT INTO client_downloads (
- platform_type,
- platform_name,
- version,
- version_code,
- download_url,
- file_size,
- release_notes,
- is_active,
- is_latest,
- min_system_version,
- created_by
-) VALUES
-(
- 'terminal',
- 'android',
- '1.0.0',
- 1000,
- 'https://download.imeeting.com/terminals/android/iMeeting-1.0.0-Terminal.apk',
- 25165824, -- 24MB
- '专用终端初始版本
-- 支持专用硬件集成
-- 优化的录音功能
-- 低功耗模式
-- 自动上传同步',
- TRUE,
- TRUE,
- 'Android 5.0',
- 1
-),
-
--- 单片机(MCU)专用终端
-(
- 'terminal',
- 'mcu',
- '1.0.0',
- 1000,
- 'https://download.imeeting.com/terminals/mcu/iMeeting-1.0.0-MCU.bin',
- 2097152, -- 2MB
- '单片机固件初始版本
-- 嵌入式录音系统
-- 低功耗设计
-- 支持WiFi/4G上传
-- 硬件级音频处理',
- TRUE,
- TRUE,
- 'ESP32 / STM32',
- 1
-);
+-- 专用终端设备表
+CREATE TABLE IF NOT EXISTS `terminals` (
+ `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `imei` varchar(64) NOT NULL COMMENT 'IMEI号(设备唯一标识)',
+ `terminal_name` varchar(100) DEFAULT NULL COMMENT '终端名称/设备别名',
+ `terminal_type` varchar(50) NOT NULL COMMENT '终端类型(关联dict_data.dict_code)',
+ `description` varchar(500) DEFAULT NULL COMMENT '终端说明/备注',
+
+ -- 状态管理
+ `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '启用状态: 1-启用, 0-停用',
+ `is_activated` tinyint(1) NOT NULL DEFAULT '0' COMMENT '激活状态: 1-已激活, 0-未激活',
+ `activated_at` datetime DEFAULT NULL COMMENT '激活时间',
+
+ -- 运维监控字段
+ `firmware_version` varchar(50) DEFAULT NULL COMMENT '当前固件版本',
+ `last_online_at` datetime DEFAULT NULL COMMENT '最后在线/心跳时间',
+ `ip_address` varchar(50) DEFAULT NULL COMMENT '最近一次连接IP',
+ `mac_address` varchar(64) DEFAULT NULL COMMENT 'MAC地址',
+
+ -- 审计字段
+ `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '录入时间',
+ `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `created_by` int(11) DEFAULT NULL COMMENT '录入人ID',
+
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_imei` (`imei`),
+ KEY `idx_terminal_type` (`terminal_type`),
+ KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='专用终端设备表';
\ No newline at end of file
diff --git a/database.md b/database.md
index 738b913..ec2bc83 100644
--- a/database.md
+++ b/database.md
@@ -20,6 +20,7 @@
- **knowledge_bases_task**: 知识库生成任务表
- **dict_data**: 字典/码表数据表
- **client_downloads**: 客户端下载管理表
+- **terminals**: 专用终端设备表
---
@@ -309,14 +310,29 @@
- `platform_code` 关联 `dict_data` 表的 `dict_code` 字段(client_platform类型)
- 业务逻辑需确保:设置新最新版本时,自动取消同平台其他版本的最新状态
-**相关API接口:**
-- `GET /api/clients/latest/by-platform` - 获取最新版本客户端
- - 支持两种调用方式(兼容新旧版本,返回数据结构一致):
- 1. 旧版:传 `platform_type` 和 `platform_name` 参数
- 2. 新版:传 `platform_code` 参数(推荐)
-- `POST /api/clients/upload` - 上传客户端安装包(管理员)
- - 自动解析APK文件的版本信息
- - 自动读取文件大小并生成下载URL
+### 2.18. `terminals` - 专用终端设备表
+
+存储专用终端设备信息(如录音笔、会议平板等),用于设备激活管理和状态监控。
+
+| 字段名 | 类型 | 约束 | 描述 |
+| :--- | :--- | :--- | :--- |
+| `id` | INT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
+| `imei` | VARCHAR(64) | NOT NULL, UNIQUE | IMEI号(设备唯一标识) |
+| `terminal_name` | VARCHAR(100) | NULL | 终端名称/设备别名 |
+| `terminal_type` | VARCHAR(50) | NOT NULL | 终端类型(关联 `dict_data.dict_code`) |
+| `description` | VARCHAR(500) | NULL | 终端说明/备注 |
+| `status` | TINYINT(1) | NOT NULL, DEFAULT 1 | 启用状态: 1-启用, 0-停用 |
+| `is_activated` | TINYINT(1) | NOT NULL, DEFAULT 0 | 激活状态: 1-已激活, 0-未激活 |
+| `activated_at` | DATETIME | NULL | 激活时间 |
+| `firmware_version` | VARCHAR(50) | NULL | 当前固件版本 |
+| `last_online_at` | DATETIME | NULL | 最后在线/心跳时间 |
+| `ip_address` | VARCHAR(50) | NULL | 最近一次连接IP |
+| `mac_address` | VARCHAR(64) | NULL | MAC地址 |
+| `created_at` | DATETIME | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 录入时间 |
+| `updated_at` | DATETIME | NOT NULL, DEFAULT CURRENT_TIMESTAMP ON UPDATE | 更新时间 |
+| `created_by` | INT | NULL, FK | 录入人ID(关联 `users` 表) |
+| | | KEY `idx_terminal_type` | 终端类型索引 |
+| | | KEY `idx_status` | 状态索引 |
---
@@ -399,6 +415,14 @@ erDiagram
int progress
}
+ dedicated_terminals {
+ int id PK
+ varchar(64) imei
+ varchar(50) terminal_type
+ tinyint status
+ tinyint is_activated
+ }
+
users ||--o{ meetings : "creates"
users ||--o{ attendees : "attends"
users }|..|| roles : "has role"
@@ -409,4 +433,4 @@ erDiagram
meetings ||--|{ meeting_summaries : "has"
meetings ||--|{ llm_tasks : "has"
-```
+```
\ No newline at end of file
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
index cc9275d..b5ed56b 100644
--- a/frontend/public/favicon.svg
+++ b/frontend/public/favicon.svg
@@ -1,26 +1 @@
-
+
\ No newline at end of file
diff --git a/frontend/src/config/api.js b/frontend/src/config/api.js
index 6be55dd..fc344f8 100644
--- a/frontend/src/config/api.js
+++ b/frontend/src/config/api.js
@@ -104,6 +104,14 @@ const API_CONFIG = {
TEMPLATE: '/api/voiceprint/template',
UPLOAD: (userId) => `/api/voiceprint/${userId}`,
DELETE: (userId) => `/api/voiceprint/${userId}`
+ },
+ TERMINALS: {
+ LIST: '/api/terminals',
+ CREATE: '/api/terminals',
+ DETAIL: (id) => `/api/terminals/${id}`,
+ UPDATE: (id) => `/api/terminals/${id}`,
+ DELETE: (id) => `/api/terminals/${id}`,
+ STATUS: (id) => `/api/terminals/${id}/status`
}
}
};
diff --git a/frontend/src/pages/AdminManagement.jsx b/frontend/src/pages/AdminManagement.jsx
index 0546ea8..6f29441 100644
--- a/frontend/src/pages/AdminManagement.jsx
+++ b/frontend/src/pages/AdminManagement.jsx
@@ -1,9 +1,10 @@
import React from 'react';
-import { Settings, Users, Smartphone, Shield, BookText, Type, Package } from 'lucide-react';
+import { Settings, Users, Smartphone, Shield, BookText, Type, Package, Monitor } from 'lucide-react';
import { Tabs } from 'antd';
import UserManagement from './admin/UserManagement';
import ClientManagement from './ClientManagement';
import ExternalAppManagement from './admin/ExternalAppManagement';
+import TerminalManagement from './admin/TerminalManagement';
import PermissionManagement from './admin/PermissionManagement';
import DictManagement from './admin/DictManagement';
import HotWordManagement from './admin/HotWordManagement';
@@ -44,6 +45,11 @@ const AdminManagement = () => {
label:
管理专用终端设备的接入与状态
+| IMEI | +终端名称 | +类型 | +状态 | +激活状态 | +最后在线 | +创建时间 | +操作 | +
|---|---|---|---|---|---|---|---|
| 暂无数据 | +|||||||
| {terminal.imei} | +{terminal.terminal_name || '-'} | ++ + {getTerminalTypeLabel(terminal.terminal_type)} + + | +
+ handleToggleStatus(terminal)}>
+
+
+
+
+ {terminal.status === 1 ? '启用' : '停用'}
+ |
+ + + {terminal.is_activated === 1 ? '已激活' : '未激活'} + | +{terminal.last_online_at ? new Date(terminal.last_online_at).toLocaleString() : '-'} | +{new Date(terminal.created_at).toLocaleDateString()} | +
+
+
+
+
+ |
+