270 lines
10 KiB
Python
270 lines
10 KiB
Python
"""
|
||
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)
|