enterprise-saa-s-dashboard-.../python_backend/main.py

270 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
main.py
FastAPI 主应用:设备管理平台后端服务。
启动方式:
uvicorn main:app --reload --host 0.0.0.0 --port 8000
访问文档:
http://localhost:8000/docs (Swagger UI)
http://localhost:8000/redoc (ReDoc)
"""
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from contextlib import asynccontextmanager
from database import init_db, seed_demo_data, get_db
from models import (
DeviceCheckRequest,
LicenseGenerateRequest,
ActivateReportRequest,
CheckResponse,
LicenseResponse,
ActivateResponse,
DeviceInfo,
)
from services import build_license_data, encrypt_license, decrypt_license
# ═══════════════════════════════════════════════════════════════
# 生命周期:启动时初始化数据库
# ═══════════════════════════════════════════════════════════════
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用启动时自动创建表并插入演示数据"""
init_db()
seed_demo_data()
yield
# 关闭时可做清理(此处无需)
app = FastAPI(
title="设备管理平台 API",
description="基于 FastAPI + SQLite 的轻量级设备管理后端,支持设备校验、授权文件生成、激活上报。",
version="1.0.0",
lifespan=lifespan,
)
# 允许跨域(方便前端/APP 调试)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# ═══════════════════════════════════════════════════════════════
# 根路由:自动跳转到 Swagger 文档
# ═══════════════════════════════════════════════════════════════
@app.get("/", include_in_schema=False)
def root():
return RedirectResponse(url="/docs")
# ═══════════════════════════════════════════════════════════════
# 1. 设备校验接口
# ═══════════════════════════════════════════════════════════════
@app.post("/api/devices/check", response_model=CheckResponse, summary="设备校验")
def device_check(payload: DeviceCheckRequest):
"""
APP 启动时调用,校验设备 SN 是否合法、是否已激活。
- 若 SN 不存在 → 非法设备,拒绝激活
- 若 SN 存在但未激活 → 允许进入激活流程
- 若 SN 已激活 → 正常使用
"""
with get_db() as db:
row = db.execute(
"SELECT status FROM devices WHERE sn = ?", (payload.sn,)
).fetchone()
if not row:
return CheckResponse(
valid=False,
activated=False,
message="设备 SN 不存在,请联系管理员注册"
)
status = row["status"]
if status == "已激活":
return CheckResponse(
valid=True,
activated=True,
message="设备已激活,正常使用"
)
elif status == "已禁用":
return CheckResponse(
valid=True,
activated=False,
message="设备已被禁用,请联系客服"
)
else:
return CheckResponse(
valid=True,
activated=False,
message="设备待激活,请获取授权文件并激活"
)
# ═══════════════════════════════════════════════════════════════
# 2. 生成加密授权文件接口
# ═══════════════════════════════════════════════════════════════
@app.post("/api/licenses/generate", response_model=LicenseResponse, summary="生成加密授权文件")
def generate_license(payload: LicenseGenerateRequest):
"""
管理后台调用,为指定设备生成绑定 SN 的加密授权文件。
流程:
1. 校验设备 SN 是否存在
2. 构造授权数据(模块列表 + 有效期)
3. XOR 加密(密钥由 SN 派生,一机一密)
4. 返回 Base64 加密字符串
APP 收到后必须用相同 SN 才能解密。
"""
with get_db() as db:
row = db.execute(
"SELECT sn FROM devices WHERE sn = ?", (payload.sn,)
).fetchone()
if not row:
raise HTTPException(status_code=404, detail="设备 SN 不存在")
# 构造明文授权数据
license_data = build_license_data(
device_sn=payload.sn,
modules=payload.modules,
valid_days=payload.valid_days,
)
# 加密(绑定 SN
encrypted = encrypt_license(license_data)
return LicenseResponse(
success=True,
encrypted_license=encrypted,
raw_license=license_data.model_dump(), # 调试用,生产环境可去掉
message="授权文件生成成功,已绑定设备 SN"
)
# ═══════════════════════════════════════════════════════════════
# 3. 上报激活状态接口
# ═══════════════════════════════════════════════════════════════
@app.post("/api/devices/activate", response_model=ActivateResponse, summary="上报激活状态")
def report_activation(payload: ActivateReportRequest):
"""
设备主机首次联网后调用,上报激活结果。
- 若上报 "已激活" → 数据库更新状态与激活时间
- 若上报 "激活失败" → 状态保持原样,记录失败
"""
with get_db() as db:
row = db.execute(
"SELECT status FROM devices WHERE sn = ?", (payload.sn,)
).fetchone()
if not row:
raise HTTPException(status_code=404, detail="设备 SN 不存在")
current_status = row["status"]
# 只有"待激活"或"已禁用"的设备允许重新激活
if payload.status == "已激活":
db.execute(
"""
UPDATE devices
SET status = '已激活',
activated_at = datetime('now', 'localtime')
WHERE sn = ?
""",
(payload.sn,),
)
db.commit()
# 查询更新后的时间
updated = db.execute(
"SELECT activated_at FROM devices WHERE sn = ?", (payload.sn,)
).fetchone()
return ActivateResponse(
success=True,
sn=payload.sn,
new_status="已激活",
activated_at=updated["activated_at"],
message="设备激活成功,已记录激活时间"
)
else:
return ActivateResponse(
success=False,
sn=payload.sn,
new_status=current_status,
message="激活失败,未更新状态"
)
# ═══════════════════════════════════════════════════════════════
# 辅助接口(方便调试和管理)
# ═══════════════════════════════════════════════════════════════
@app.get("/api/devices", summary="获取所有设备列表")
def list_devices():
"""列出当前所有设备及其状态"""
with get_db() as db:
rows = db.execute(
"SELECT sn, status, activated_at, created_at FROM devices ORDER BY created_at DESC"
).fetchall()
return [dict(r) for r in rows]
@app.post("/api/devices/register", summary="注册新设备")
def register_device(sn: str):
"""向平台注册一个新的设备 SN"""
with get_db() as db:
try:
db.execute(
"INSERT INTO devices (sn, status) VALUES (?, '待激活')",
(sn,),
)
db.commit()
return {"success": True, "sn": sn, "message": "设备注册成功"}
except Exception:
raise HTTPException(status_code=400, detail="设备 SN 已存在")
@app.post("/api/licenses/verify", summary="验证授权文件")
def verify_license(sn: str, encrypted_license: str):
"""
调试验证:传入加密授权文件和设备 SN验证能否正确解密。
用于测试一机一密绑定是否生效。
"""
decrypted = decrypt_license(encrypted_license, sn)
if decrypted:
return {
"valid": True,
"decrypted": decrypted,
"message": "授权文件解密成功SN 匹配"
}
return {
"valid": False,
"decrypted": None,
"message": "授权文件无效或 SN 不匹配"
}
# ═══════════════════════════════════════════════════════════════
# 运行入口(直接 python main.py 启动)
# ═══════════════════════════════════════════════════════════════
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="localhost", port=8000, reload=True)