重构了设备管理模块、
parent
883a1a2f1b
commit
b60b4a7b6b
|
|
@ -0,0 +1,81 @@
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from fastapi import Request, Response
|
||||||
|
import time
|
||||||
|
from app.services.terminal_service import terminal_service
|
||||||
|
from app.services.jwt_service import jwt_service
|
||||||
|
from app.core.response import create_api_response
|
||||||
|
|
||||||
|
class TerminalCheckMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(self, request: Request, call_next):
|
||||||
|
# 1. 检查是否有 Imei 头,没有则认为是普通请求,直接放行
|
||||||
|
imei = request.headers.get("Imei")
|
||||||
|
if not imei:
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
# 2. 检查时间戳 (防重放/时钟同步)
|
||||||
|
# 优先从Header获取,如果没有则尝试从Query Parameter获取
|
||||||
|
client_time_str = request.headers.get("time") or request.query_params.get("time")
|
||||||
|
|
||||||
|
if client_time_str:
|
||||||
|
try:
|
||||||
|
client_time = int(client_time_str)
|
||||||
|
server_time = int(time.time() * 1000)
|
||||||
|
|
||||||
|
# 允许 10 分钟的误差 (10 * 60 * 1000 = 600000 ms)
|
||||||
|
# 考虑到网络延迟和设备时间未校准,设置宽松一点
|
||||||
|
if abs(server_time - client_time) > 600000:
|
||||||
|
return create_api_response(
|
||||||
|
code="400",
|
||||||
|
message="设备时间与服务器时间差距过大,请校准时间"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
# 时间格式错误,暂时忽略或返回错误
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 3. 提取其他设备信息
|
||||||
|
device_type = request.headers.get("deviceType", "UNKNOWN")
|
||||||
|
# device_info 可能是 "UNIS iMeeting a7"
|
||||||
|
device_info = request.headers.get("deviceInfo", "Unknown Device")
|
||||||
|
|
||||||
|
# 获取客户端IP (考虑代理)
|
||||||
|
client_ip = request.client.host
|
||||||
|
if "x-forwarded-for" in request.headers:
|
||||||
|
client_ip = request.headers["x-forwarded-for"].split(",")[0].strip()
|
||||||
|
elif "x-real-ip" in request.headers:
|
||||||
|
client_ip = request.headers["x-real-ip"]
|
||||||
|
|
||||||
|
# 获取当前用户ID (如果已登录)
|
||||||
|
user_id = None
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
if auth_header and auth_header.startswith("Bearer "):
|
||||||
|
try:
|
||||||
|
token = auth_header.split(" ")[1]
|
||||||
|
payload = jwt_service.verify_token(token)
|
||||||
|
if payload:
|
||||||
|
user_id = payload.get("user_id")
|
||||||
|
except Exception:
|
||||||
|
pass # 忽略token解析错误,只记录设备在线状态
|
||||||
|
|
||||||
|
# 4. 调用服务进行检查和更新
|
||||||
|
# 注意:这里是同步调用数据库,但在 async 中可能会阻塞 loop
|
||||||
|
# 理想情况下 terminal_service 应该是 async 的,或者使用 run_in_executor
|
||||||
|
# 但由于数据库操作较快,且 mysql-connector 是同步的,暂时直接调用
|
||||||
|
# 如果并发高,建议将 service 改为 async
|
||||||
|
|
||||||
|
result = terminal_service.check_and_update_terminal(
|
||||||
|
imei=imei,
|
||||||
|
terminal_type=device_type,
|
||||||
|
terminal_name=device_info,
|
||||||
|
ip_address=client_ip,
|
||||||
|
user_id=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result["allowed"]:
|
||||||
|
return create_api_response(
|
||||||
|
code="403",
|
||||||
|
message=result["reason"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. 放行请求
|
||||||
|
response = await call_next(request)
|
||||||
|
return response
|
||||||
|
|
@ -14,6 +14,7 @@ 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 fastapi.openapi.docs import get_swagger_ui_html
|
from fastapi.openapi.docs import get_swagger_ui_html
|
||||||
|
from app.core.middleware import TerminalCheckMiddleware
|
||||||
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.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
|
from app.core.config import UPLOAD_DIR, API_CONFIG
|
||||||
|
|
||||||
|
|
@ -25,6 +26,9 @@ app = FastAPI(
|
||||||
redoc_url=None
|
redoc_url=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 添加终端检查中间件 (在CORS之前添加,以便位于CORS内部)
|
||||||
|
app.add_middleware(TerminalCheckMiddleware)
|
||||||
|
|
||||||
# 添加CORS中间件
|
# 添加CORS中间件
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,12 @@ class TerminalService:
|
||||||
SELECT
|
SELECT
|
||||||
t.*,
|
t.*,
|
||||||
u.username as creator_username,
|
u.username as creator_username,
|
||||||
|
cu.username as current_username,
|
||||||
|
cu.caption as current_user_caption,
|
||||||
dd.label_cn as terminal_type_name
|
dd.label_cn as terminal_type_name
|
||||||
FROM terminals t
|
FROM terminals t
|
||||||
LEFT JOIN users u ON t.created_by = u.user_id
|
LEFT JOIN users u ON t.created_by = u.user_id
|
||||||
|
LEFT JOIN users cu ON t.current_user_id = cu.user_id
|
||||||
LEFT JOIN dict_data dd ON t.terminal_type = dd.dict_code AND dd.dict_type = 'terminal_type'
|
LEFT JOIN dict_data dd ON t.terminal_type = dd.dict_code AND dd.dict_type = 'terminal_type'
|
||||||
WHERE {where_clause}
|
WHERE {where_clause}
|
||||||
ORDER BY t.created_at DESC
|
ORDER BY t.created_at DESC
|
||||||
|
|
@ -188,4 +191,61 @@ class TerminalService:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
def check_and_update_terminal(self, imei: str, terminal_type: str, terminal_name: str, ip_address: str, user_id: Optional[int] = None) -> dict:
|
||||||
|
"""
|
||||||
|
检查并更新终端状态(中间件调用)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with get_db_connection() as conn:
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
# 检查是否存在
|
||||||
|
cursor.execute("SELECT id, status, is_activated FROM terminals WHERE imei = %s", (imei,))
|
||||||
|
existing = cursor.fetchone()
|
||||||
|
|
||||||
|
current_time = datetime.datetime.now()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# 检查是否被停用
|
||||||
|
if existing['status'] == 0:
|
||||||
|
return {"allowed": False, "reason": "设备已被停用", "terminal_id": existing['id']}
|
||||||
|
|
||||||
|
# 更新在线时间、IP、名称(如果变了)、当前用户
|
||||||
|
update_query = """
|
||||||
|
UPDATE terminals
|
||||||
|
SET last_online_at = %s,
|
||||||
|
ip_address = %s,
|
||||||
|
current_user_id = %s,
|
||||||
|
# 如果设备没名字,尝试用上报的名字填充
|
||||||
|
terminal_name = IF(terminal_name IS NULL OR terminal_name = '', %s, terminal_name),
|
||||||
|
# 如果设备没类型,尝试用上报的类型填充
|
||||||
|
terminal_type = IF(terminal_type IS NULL OR terminal_type = '', %s, terminal_type)
|
||||||
|
WHERE id = %s
|
||||||
|
"""
|
||||||
|
cursor.execute(update_query, (current_time, ip_address, user_id, terminal_name, terminal_type, existing['id']))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return {"allowed": True, "reason": "", "terminal_id": existing['id']}
|
||||||
|
else:
|
||||||
|
# 新设备自动注册并激活
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO terminals (
|
||||||
|
imei, terminal_name, terminal_type, status,
|
||||||
|
is_activated, activated_at, last_online_at, ip_address, created_at, current_user_id
|
||||||
|
) VALUES (%s, %s, %s, 1, 1, %s, %s, %s, %s, %s)
|
||||||
|
"""
|
||||||
|
cursor.execute(insert_query, (
|
||||||
|
imei, terminal_name, terminal_type,
|
||||||
|
current_time, current_time, ip_address, current_time, user_id
|
||||||
|
))
|
||||||
|
new_id = cursor.lastrowid
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return {"allowed": True, "reason": "", "terminal_id": new_id}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in check_and_update_terminal: {e}")
|
||||||
|
# 数据库错误暂时放行,或者根据安全策略决定
|
||||||
|
return {"allowed": True, "reason": f"DB Error: {str(e)}", "terminal_id": None}
|
||||||
|
|
||||||
terminal_service = TerminalService()
|
terminal_service = TerminalService()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
-- 为terminals表添加当前绑定用户ID字段
|
||||||
|
ALTER TABLE `terminals`
|
||||||
|
ADD COLUMN `current_user_id` INT DEFAULT NULL COMMENT '当前绑定/使用的用户ID',
|
||||||
|
ADD CONSTRAINT `fk_terminals_current_user` FOREIGN KEY (`current_user_id`) REFERENCES `users` (`user_id`) ON DELETE SET NULL;
|
||||||
|
|
@ -215,6 +215,13 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.truncate-cell {
|
||||||
|
max-width: 180px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.data-table tr:last-child td {
|
.data-table tr:last-child td {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,7 @@ const TerminalManagement = () => {
|
||||||
<th>IMEI</th>
|
<th>IMEI</th>
|
||||||
<th>终端名称</th>
|
<th>终端名称</th>
|
||||||
<th>类型</th>
|
<th>类型</th>
|
||||||
|
<th>当前绑定账号</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>激活状态</th>
|
<th>激活状态</th>
|
||||||
<th>最后在线</th>
|
<th>最后在线</th>
|
||||||
|
|
@ -293,18 +294,28 @@ const TerminalManagement = () => {
|
||||||
<tbody>
|
<tbody>
|
||||||
{paginatedTerminals.length === 0 ? (
|
{paginatedTerminals.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan="8" className="empty-state">暂无数据</td>
|
<td colSpan="9" className="empty-state">暂无数据</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
paginatedTerminals.map(terminal => (
|
paginatedTerminals.map(terminal => (
|
||||||
<tr key={terminal.id}>
|
<tr key={terminal.id}>
|
||||||
<td className="font-mono">{terminal.imei}</td>
|
<td className="font-mono truncate-cell" title={terminal.imei}>{terminal.imei}</td>
|
||||||
<td>{terminal.terminal_name || '-'}</td>
|
<td className="truncate-cell" title={terminal.terminal_name}>{terminal.terminal_name || '-'}</td>
|
||||||
<td>
|
<td>
|
||||||
<span className="type-badge">
|
<span className="type-badge">
|
||||||
{getTerminalTypeLabel(terminal.terminal_type)}
|
{getTerminalTypeLabel(terminal.terminal_type)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{terminal.current_user_caption ? (
|
||||||
|
<div className="user-info truncate-cell" title={`${terminal.current_user_caption} (${terminal.current_username || ''})`}>
|
||||||
|
<span className="user-name">{terminal.current_user_caption}</span>
|
||||||
|
{terminal.current_username && <span className="user-sub text-xs text-gray-500">({terminal.current_username})</span>}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400">-</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="status-toggle" onClick={() => handleToggleStatus(terminal)}>
|
<div className="status-toggle" onClick={() => handleToggleStatus(terminal)}>
|
||||||
<div className={`toggle-switch ${terminal.status === 1 ? 'on' : 'off'}`}>
|
<div className={`toggle-switch ${terminal.status === 1 ? 'on' : 'off'}`}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue