Initial commit: 000-platform project skeleton
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
39
backend/app/config.py
Normal file
39
backend/app/config.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""配置管理"""
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""应用配置"""
|
||||
# 应用信息
|
||||
APP_NAME: str = "000-platform"
|
||||
APP_VERSION: str = "0.1.0"
|
||||
DEBUG: bool = False
|
||||
|
||||
# 数据库
|
||||
DATABASE_URL: str = "mysql+pymysql://scrm_reader:ScrmReader2024Pass@47.107.71.55:3306/new_qiqi"
|
||||
|
||||
# API Key(内部服务调用)
|
||||
API_KEY: str = "platform_api_key_2026"
|
||||
|
||||
# 管理员账号
|
||||
ADMIN_USERNAME: str = "admin"
|
||||
ADMIN_PASSWORD_HASH: str = "" # bcrypt hash
|
||||
|
||||
# JWT
|
||||
JWT_SECRET: str = "platform_jwt_secret_2026_change_in_prod"
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
JWT_EXPIRE_HOURS: int = 24
|
||||
|
||||
# 配置加密密钥
|
||||
CONFIG_ENCRYPT_KEY: str = "platform_config_key_32bytes!!"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
29
backend/app/database.py
Normal file
29
backend/app/database.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""数据库连接"""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from .config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
engine = create_engine(
|
||||
settings.DATABASE_URL,
|
||||
pool_pre_ping=True,
|
||||
pool_size=5,
|
||||
max_overflow=10,
|
||||
echo=settings.DEBUG
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db():
|
||||
"""获取数据库会话"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
38
backend/app/main.py
Normal file
38
backend/app/main.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""平台服务入口"""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from .config import get_settings
|
||||
from .routers import stats_router, logs_router, config_router, health_router
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.APP_VERSION,
|
||||
description="平台基础设施服务 - 统计/日志/配置管理"
|
||||
)
|
||||
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 注册路由
|
||||
app.include_router(health_router)
|
||||
app.include_router(stats_router)
|
||||
app.include_router(logs_router)
|
||||
app.include_router(config_router)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"service": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION,
|
||||
"docs": "/docs"
|
||||
}
|
||||
13
backend/app/models/__init__.py
Normal file
13
backend/app/models/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""数据模型"""
|
||||
from .tenant import Tenant, Subscription, Config
|
||||
from .stats import AICallEvent, TenantUsageDaily
|
||||
from .logs import PlatformLog
|
||||
|
||||
__all__ = [
|
||||
"Tenant",
|
||||
"Subscription",
|
||||
"Config",
|
||||
"AICallEvent",
|
||||
"TenantUsageDaily",
|
||||
"PlatformLog"
|
||||
]
|
||||
31
backend/app/models/logs.py
Normal file
31
backend/app/models/logs.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""日志相关模型"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, BigInteger, String, Integer, Text, JSON, Enum, TIMESTAMP
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class PlatformLog(Base):
|
||||
"""统一日志表"""
|
||||
__tablename__ = "platform_logs"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
trace_id = Column(String(36))
|
||||
tenant_id = Column(BigInteger)
|
||||
user_id = Column(BigInteger)
|
||||
app_code = Column(String(50), nullable=False)
|
||||
log_type = Column(Enum('request', 'error', 'app', 'biz', 'audit'), nullable=False)
|
||||
level = Column(Enum('debug', 'info', 'warn', 'error', 'fatal'), default='info')
|
||||
category = Column(String(100))
|
||||
message = Column(Text, nullable=False)
|
||||
context = Column(JSON)
|
||||
method = Column(String(10))
|
||||
path = Column(String(500))
|
||||
status_code = Column(Integer)
|
||||
duration_ms = Column(Integer)
|
||||
error_type = Column(String(100))
|
||||
stack_trace = Column(Text)
|
||||
action = Column(String(100))
|
||||
target_type = Column(String(50))
|
||||
target_id = Column(String(100))
|
||||
log_time = Column(TIMESTAMP, nullable=False)
|
||||
created_at = Column(TIMESTAMP, default=datetime.now)
|
||||
41
backend/app/models/stats.py
Normal file
41
backend/app/models/stats.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""统计相关模型"""
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import Column, BigInteger, String, Integer, Date, DECIMAL, TIMESTAMP
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class AICallEvent(Base):
|
||||
"""AI调用明细"""
|
||||
__tablename__ = "platform_ai_call_events"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
tenant_id = Column(BigInteger, nullable=False)
|
||||
user_id = Column(BigInteger)
|
||||
app_code = Column(String(50), nullable=False)
|
||||
module_code = Column(String(50), nullable=False)
|
||||
trace_id = Column(String(36))
|
||||
prompt_name = Column(String(100), nullable=False)
|
||||
model = Column(String(100), nullable=False)
|
||||
input_tokens = Column(Integer, default=0)
|
||||
output_tokens = Column(Integer, default=0)
|
||||
cost = Column(DECIMAL(10, 6), default=0)
|
||||
latency_ms = Column(Integer, default=0)
|
||||
status = Column(String(20), default='success')
|
||||
event_time = Column(TIMESTAMP, nullable=False)
|
||||
created_at = Column(TIMESTAMP, default=datetime.now)
|
||||
|
||||
|
||||
class TenantUsageDaily(Base):
|
||||
"""租户日用量汇总"""
|
||||
__tablename__ = "platform_tenant_usage_daily"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
tenant_id = Column(BigInteger, nullable=False)
|
||||
app_code = Column(String(50), nullable=False)
|
||||
stat_date = Column(Date, nullable=False)
|
||||
ai_calls = Column(Integer, default=0)
|
||||
ai_tokens = Column(BigInteger, default=0)
|
||||
ai_cost = Column(DECIMAL(10, 4), default=0)
|
||||
created_at = Column(TIMESTAMP, default=datetime.now)
|
||||
updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now)
|
||||
47
backend/app/models/tenant.py
Normal file
47
backend/app/models/tenant.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""租户相关模型"""
|
||||
from datetime import datetime, date
|
||||
from sqlalchemy import Column, BigInteger, String, Enum, Date, Text, Boolean, JSON, TIMESTAMP
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class Tenant(Base):
|
||||
"""租户表"""
|
||||
__tablename__ = "platform_tenants"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
code = Column(String(50), unique=True, nullable=False)
|
||||
name = Column(String(100), nullable=False)
|
||||
contact_info = Column(JSON)
|
||||
status = Column(Enum('active', 'expired', 'trial'), default='active')
|
||||
expired_at = Column(Date)
|
||||
created_at = Column(TIMESTAMP, default=datetime.now)
|
||||
updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class Subscription(Base):
|
||||
"""订阅表"""
|
||||
__tablename__ = "platform_subscriptions"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
tenant_id = Column(BigInteger, nullable=False)
|
||||
app_code = Column(String(50), nullable=False)
|
||||
start_date = Column(Date)
|
||||
end_date = Column(Date)
|
||||
quota = Column(JSON)
|
||||
status = Column(Enum('active', 'expired'), default='active')
|
||||
created_at = Column(TIMESTAMP, default=datetime.now)
|
||||
updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class Config(Base):
|
||||
"""配置表"""
|
||||
__tablename__ = "platform_configs"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
tenant_id = Column(BigInteger, nullable=False)
|
||||
config_type = Column(String(50), nullable=False)
|
||||
config_key = Column(String(100), nullable=False)
|
||||
config_value = Column(Text)
|
||||
is_encrypted = Column(Boolean, default=False)
|
||||
created_at = Column(TIMESTAMP, default=datetime.now)
|
||||
updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now)
|
||||
12
backend/app/routers/__init__.py
Normal file
12
backend/app/routers/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""API路由"""
|
||||
from .stats import router as stats_router
|
||||
from .logs import router as logs_router
|
||||
from .config import router as config_router
|
||||
from .health import router as health_router
|
||||
|
||||
__all__ = [
|
||||
"stats_router",
|
||||
"logs_router",
|
||||
"config_router",
|
||||
"health_router"
|
||||
]
|
||||
49
backend/app/routers/config.py
Normal file
49
backend/app/routers/config.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""配置路由"""
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from ..config import get_settings
|
||||
from ..models.tenant import Config
|
||||
from ..schemas.config import ConfigValue, ConfigWrite
|
||||
from ..services.crypto import decrypt_value
|
||||
|
||||
router = APIRouter(prefix="/api/config", tags=["config"])
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
def verify_api_key(x_api_key: str = Header(..., alias="X-API-Key")):
|
||||
"""验证API Key"""
|
||||
if x_api_key != settings.API_KEY:
|
||||
raise HTTPException(status_code=401, detail="Invalid API Key")
|
||||
return x_api_key
|
||||
|
||||
|
||||
@router.get("/{config_type}/{config_key}", response_model=ConfigValue)
|
||||
async def get_config(
|
||||
config_type: str,
|
||||
config_key: str,
|
||||
tenant_id: int = Query(..., description="租户ID"),
|
||||
db: Session = Depends(get_db),
|
||||
_: str = Depends(verify_api_key)
|
||||
):
|
||||
"""读取配置"""
|
||||
config = db.query(Config).filter(
|
||||
Config.tenant_id == tenant_id,
|
||||
Config.config_type == config_type,
|
||||
Config.config_key == config_key
|
||||
).first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Config not found")
|
||||
|
||||
value = config.config_value
|
||||
if config.is_encrypted:
|
||||
value = decrypt_value(value)
|
||||
|
||||
return ConfigValue(
|
||||
config_type=config.config_type,
|
||||
config_key=config.config_key,
|
||||
config_value=value
|
||||
)
|
||||
17
backend/app/routers/health.py
Normal file
17
backend/app/routers/health.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""健康检查路由"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
from ..config import get_settings
|
||||
|
||||
router = APIRouter(tags=["health"])
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
return {
|
||||
"status": "ok",
|
||||
"app": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION
|
||||
}
|
||||
45
backend/app/routers/logs.py
Normal file
45
backend/app/routers/logs.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""日志路由"""
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from ..config import get_settings
|
||||
from ..models.logs import PlatformLog
|
||||
from ..schemas.logs import LogCreate, LogResponse, BatchLogRequest
|
||||
|
||||
router = APIRouter(prefix="/api/logs", tags=["logs"])
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
def verify_api_key(x_api_key: str = Header(..., alias="X-API-Key")):
|
||||
"""验证API Key"""
|
||||
if x_api_key != settings.API_KEY:
|
||||
raise HTTPException(status_code=401, detail="Invalid API Key")
|
||||
return x_api_key
|
||||
|
||||
|
||||
@router.post("/write", response_model=LogResponse)
|
||||
async def write_log(
|
||||
log: LogCreate,
|
||||
db: Session = Depends(get_db),
|
||||
_: str = Depends(verify_api_key)
|
||||
):
|
||||
"""写入日志"""
|
||||
db_log = PlatformLog(**log.model_dump())
|
||||
db.add(db_log)
|
||||
db.commit()
|
||||
db.refresh(db_log)
|
||||
return db_log
|
||||
|
||||
|
||||
@router.post("/write/batch")
|
||||
async def batch_write_logs(
|
||||
request: BatchLogRequest,
|
||||
db: Session = Depends(get_db),
|
||||
_: str = Depends(verify_api_key)
|
||||
):
|
||||
"""批量写入日志"""
|
||||
logs = [PlatformLog(**l.model_dump()) for l in request.logs]
|
||||
db.add_all(logs)
|
||||
db.commit()
|
||||
return {"success": True, "count": len(logs)}
|
||||
45
backend/app/routers/stats.py
Normal file
45
backend/app/routers/stats.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""统计上报路由"""
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from ..config import get_settings
|
||||
from ..models.stats import AICallEvent
|
||||
from ..schemas.stats import AICallEventCreate, AICallEventResponse, BatchReportRequest
|
||||
|
||||
router = APIRouter(prefix="/api/stats", tags=["stats"])
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
def verify_api_key(x_api_key: str = Header(..., alias="X-API-Key")):
|
||||
"""验证API Key"""
|
||||
if x_api_key != settings.API_KEY:
|
||||
raise HTTPException(status_code=401, detail="Invalid API Key")
|
||||
return x_api_key
|
||||
|
||||
|
||||
@router.post("/report", response_model=AICallEventResponse)
|
||||
async def report_ai_call(
|
||||
event: AICallEventCreate,
|
||||
db: Session = Depends(get_db),
|
||||
_: str = Depends(verify_api_key)
|
||||
):
|
||||
"""上报AI调用事件"""
|
||||
db_event = AICallEvent(**event.model_dump())
|
||||
db.add(db_event)
|
||||
db.commit()
|
||||
db.refresh(db_event)
|
||||
return db_event
|
||||
|
||||
|
||||
@router.post("/report/batch")
|
||||
async def batch_report_ai_calls(
|
||||
request: BatchReportRequest,
|
||||
db: Session = Depends(get_db),
|
||||
_: str = Depends(verify_api_key)
|
||||
):
|
||||
"""批量上报AI调用事件"""
|
||||
events = [AICallEvent(**e.model_dump()) for e in request.events]
|
||||
db.add_all(events)
|
||||
db.commit()
|
||||
return {"success": True, "count": len(events)}
|
||||
12
backend/app/schemas/__init__.py
Normal file
12
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""请求/响应模型"""
|
||||
from .stats import AICallEventCreate, AICallEventResponse
|
||||
from .logs import LogCreate, LogResponse
|
||||
from .config import ConfigRead
|
||||
|
||||
__all__ = [
|
||||
"AICallEventCreate",
|
||||
"AICallEventResponse",
|
||||
"LogCreate",
|
||||
"LogResponse",
|
||||
"ConfigRead"
|
||||
]
|
||||
26
backend/app/schemas/config.py
Normal file
26
backend/app/schemas/config.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""配置相关Schema"""
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ConfigRead(BaseModel):
|
||||
"""配置读取请求"""
|
||||
tenant_id: int = Field(..., description="租户ID")
|
||||
config_type: str = Field(..., description="配置类型")
|
||||
config_key: str = Field(..., description="配置键")
|
||||
|
||||
|
||||
class ConfigValue(BaseModel):
|
||||
"""配置值响应"""
|
||||
config_type: str
|
||||
config_key: str
|
||||
config_value: Optional[str]
|
||||
|
||||
|
||||
class ConfigWrite(BaseModel):
|
||||
"""配置写入"""
|
||||
tenant_id: int
|
||||
config_type: str
|
||||
config_key: str
|
||||
config_value: str
|
||||
encrypt: bool = False
|
||||
46
backend/app/schemas/logs.py
Normal file
46
backend/app/schemas/logs.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""日志相关Schema"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, Literal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class LogCreate(BaseModel):
|
||||
"""日志写入"""
|
||||
trace_id: Optional[str] = Field(None, description="链路追踪ID")
|
||||
tenant_id: Optional[int] = Field(None, description="租户ID")
|
||||
user_id: Optional[int] = Field(None, description="用户ID")
|
||||
app_code: str = Field(..., description="应用编码")
|
||||
log_type: Literal['request', 'error', 'app', 'biz', 'audit'] = Field(..., description="日志类型")
|
||||
level: Literal['debug', 'info', 'warn', 'error', 'fatal'] = Field('info', description="级别")
|
||||
category: Optional[str] = Field(None, description="分类")
|
||||
message: str = Field(..., description="消息")
|
||||
context: Optional[dict] = Field(None, description="上下文")
|
||||
method: Optional[str] = Field(None, description="HTTP方法")
|
||||
path: Optional[str] = Field(None, description="请求路径")
|
||||
status_code: Optional[int] = Field(None, description="HTTP状态码")
|
||||
duration_ms: Optional[int] = Field(None, description="耗时(ms)")
|
||||
error_type: Optional[str] = Field(None, description="错误类型")
|
||||
stack_trace: Optional[str] = Field(None, description="堆栈")
|
||||
action: Optional[str] = Field(None, description="操作")
|
||||
target_type: Optional[str] = Field(None, description="目标类型")
|
||||
target_id: Optional[str] = Field(None, description="目标ID")
|
||||
log_time: datetime = Field(..., description="日志时间")
|
||||
|
||||
|
||||
class LogResponse(BaseModel):
|
||||
"""日志响应"""
|
||||
id: int
|
||||
trace_id: Optional[str]
|
||||
app_code: str
|
||||
log_type: str
|
||||
level: str
|
||||
message: str
|
||||
log_time: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class BatchLogRequest(BaseModel):
|
||||
"""批量日志请求"""
|
||||
logs: list[LogCreate]
|
||||
42
backend/app/schemas/stats.py
Normal file
42
backend/app/schemas/stats.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""统计相关Schema"""
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class AICallEventCreate(BaseModel):
|
||||
"""AI调用事件上报"""
|
||||
tenant_id: int = Field(..., description="租户ID")
|
||||
user_id: Optional[int] = Field(None, description="用户ID")
|
||||
app_code: str = Field(..., description="应用编码")
|
||||
module_code: str = Field(..., description="模块编码")
|
||||
trace_id: Optional[str] = Field(None, description="链路追踪ID")
|
||||
prompt_name: str = Field(..., description="Prompt名称")
|
||||
model: str = Field(..., description="模型名称")
|
||||
input_tokens: int = Field(0, description="输入token")
|
||||
output_tokens: int = Field(0, description="输出token")
|
||||
cost: Decimal = Field(Decimal("0"), description="成本")
|
||||
latency_ms: int = Field(0, description="延迟(ms)")
|
||||
status: str = Field("success", description="状态")
|
||||
event_time: datetime = Field(..., description="事件时间")
|
||||
|
||||
|
||||
class AICallEventResponse(BaseModel):
|
||||
"""AI调用事件响应"""
|
||||
id: int
|
||||
tenant_id: int
|
||||
app_code: str
|
||||
prompt_name: str
|
||||
model: str
|
||||
input_tokens: int
|
||||
output_tokens: int
|
||||
event_time: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class BatchReportRequest(BaseModel):
|
||||
"""批量上报请求"""
|
||||
events: list[AICallEventCreate]
|
||||
4
backend/app/services/__init__.py
Normal file
4
backend/app/services/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""业务服务"""
|
||||
from .crypto import encrypt_value, decrypt_value
|
||||
|
||||
__all__ = ["encrypt_value", "decrypt_value"]
|
||||
37
backend/app/services/crypto.py
Normal file
37
backend/app/services/crypto.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""配置加密服务"""
|
||||
import base64
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
from ..config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
def _get_fernet() -> Fernet:
|
||||
"""获取Fernet实例"""
|
||||
# 使用PBKDF2从密钥派生32字节密钥
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=b'platform_salt_2026',
|
||||
iterations=100000,
|
||||
)
|
||||
key = base64.urlsafe_b64encode(kdf.derive(settings.CONFIG_ENCRYPT_KEY.encode()))
|
||||
return Fernet(key)
|
||||
|
||||
|
||||
def encrypt_value(value: str) -> str:
|
||||
"""加密配置值"""
|
||||
f = _get_fernet()
|
||||
encrypted = f.encrypt(value.encode())
|
||||
return base64.urlsafe_b64encode(encrypted).decode()
|
||||
|
||||
|
||||
def decrypt_value(encrypted_value: str) -> str:
|
||||
"""解密配置值"""
|
||||
f = _get_fernet()
|
||||
encrypted = base64.urlsafe_b64decode(encrypted_value.encode())
|
||||
decrypted = f.decrypt(encrypted)
|
||||
return decrypted.decode()
|
||||
11
backend/requirements.txt
Normal file
11
backend/requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
fastapi>=0.109.0
|
||||
uvicorn>=0.27.0
|
||||
sqlalchemy>=2.0.0
|
||||
pymysql>=1.1.0
|
||||
pydantic>=2.0.0
|
||||
pydantic-settings>=2.0.0
|
||||
cryptography>=42.0.0
|
||||
python-jose[cryptography]>=3.3.0
|
||||
passlib[bcrypt]>=1.7.4
|
||||
python-multipart>=0.0.6
|
||||
httpx>=0.26.0
|
||||
Reference in New Issue
Block a user