1. 精简功能,去除用户管理和鉴权;
2. 优化首页 已完成首页的优化工作,主要改进包括: - 优化了统计卡片的设计,添加了渐变背景和精致的阴影效果 - 改进了图标和数字的视觉效果,增加了文字阴影和图标动画 - 添加了数据趋势指标,以更直观的方式展示数据状态 - 优化了快速入门指南的样式,增加了悬停效果和平滑过渡动画 - 调整了响应式布局的间距和尺寸,确保在各种设备上都能良好显示
This commit is contained in:
parent
c469d57fb6
commit
aa2fc6d84d
3
.gitignore
vendored
3
.gitignore
vendored
@ -169,3 +169,6 @@ cython_debug/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# other file
|
||||
*.log
|
||||
161
README.md
161
README.md
@ -1,154 +1,23 @@
|
||||
# 配置中心开发
|
||||
基于fastapi开发的配置中心API,统一管理各类配置,实现配置的集中化管理与便捷操作,并提供了简单的Web界面。
|
||||
|
||||
## **一、项目背景与目标**
|
||||
## **背景与目标**
|
||||
|
||||
随着项目不断扩展,各类配置(如环境变量、服务参数、数据库连接等)日益增多且复杂。为实现配置的集中化管理与便捷操作,需开发一款配置中心,使团队成员能够高效地查看、修改、新增和删除配置项,降低因配置分散导致的维护成本和出错风险,提升研发效率与系统稳定性。
|
||||
|
||||
## **二、功能模块与详细需求**
|
||||
## **功能**
|
||||
|
||||
### 1. 配置项管理
|
||||
1. **配置管理**:支持配置的增删改查,包括环境变量、服务参数、数据库连接等。
|
||||
2. **权限控制**:实现用户角色与权限管理,确保配置安全。
|
||||
3. **版本控制**:记录配置的修改历史,便于回滚。
|
||||
4. **Web界面**:提供简洁易用的Web界面,方便用户操作。
|
||||
5. **API接口**:提供RESTful API接口,支持外部系统集成。
|
||||
|
||||
- **增删改查基础功能**
|
||||
- **创建配置项**:支持手动创建新的配置项,至少包含以下字段:
|
||||
- **类型(type_name)**:必填字段,作为配置项的分类标识。默认值为 `default`,用户可自定义类型,如 `db_config`、`service_settings` 等。同时为类型添加描述,清晰说明该类型的用途。
|
||||
- **关键字(key)**:唯一标识配置项的字段,在同一类型下不允许重复。
|
||||
- **值(value)**:配置项的具体内容,可以是字符串、数字、布尔值或 JSON 格式的复杂数据。
|
||||
- **删除配置项**:提供根据配置项的唯一标识(如 `ID` 或组合键 `type+key`)删除配置的功能。
|
||||
- **修改配置项**:支持对配置项的值、描述等信息进行编辑更新。
|
||||
- **查询配置项**:可通过多种条件组合查询,包括但不限于:
|
||||
- 根据类型(type)查询该类型下的所有配置项。
|
||||
- 根据关键字(key)查询匹配的配置项。
|
||||
- 根据值(value)部分或完全匹配查询。
|
||||
|
||||
- **类型与关键字的优先级与关联关系**
|
||||
- 类型(type)具有高于关键字(key)的优先级。在查询或操作时,优先按类型筛选,再按关键字细化。
|
||||
- 同一类型下的关键字必须唯一。不同类型的配置项可以拥有相同的关键字,但其值可能不同。
|
||||
|
||||
### 2. Web 界面设计
|
||||
|
||||
- **界面整体框架**
|
||||
- 遵循现代 Web 设计理念,界面布局清晰、层次分明。采用响应式设计,适配不同设备和屏幕尺寸。
|
||||
- 提供导航栏,方便在首页、配置列表、类型管理等页面之间快速切换。
|
||||
- 界面色调搭配合理,主色调应体现专业性和科技感,兼顾用户的视觉舒适度。建议采用蓝白灰等商务科技风格。
|
||||
|
||||
- **配置列表展示页面**
|
||||
- **类型选择与切换**:在页面顶部或左侧展示类型选择器,以下拉框、标签列表或树状列表等形式呈现所有已存在的类型。点击选择类型后,页面展示该类型下的所有配置项(key-value)列表。
|
||||
- **配置项展示**:每个配置项以简洁卡片式、表格或列表形式展示,包含以下信息:
|
||||
- 类型(type)
|
||||
- 关键字(key)
|
||||
- 值(value)(若值过长,可提供折叠/展开功能)
|
||||
- 修改时间
|
||||
- 操作按钮(编辑、删除等)
|
||||
- **搜索与筛选功能**:提供便捷的搜索框,支持按类型、关键字、值进行实时模糊查询。搜索结果即时展示,同时提供筛选条件组合(如按类型筛选后,再按关键字或值进一步筛选)。
|
||||
|
||||
- **增删改操作界面**
|
||||
- **新增配置页**:提供表单形式,引导用户填写类型、关键字、值等必要信息,自动生成默认值(如默认类型为 `default`)。
|
||||
- **编辑配置页**:可快速定位到待编辑配置项,在原表单界面预填已有信息,允许用户修改相关内容后保存。
|
||||
- **删除确认提示**:在删除配置项时,弹出确认对话框,明确询问用户是否确定删除,避免误操作导致数据丢失。
|
||||
|
||||
### 3. 数据库设计与存储
|
||||
|
||||
- **SQLite 数据库表结构**
|
||||
- **类型表(types)**:
|
||||
- `type_id`:自增主键,唯一标识每个类型。
|
||||
- `type_name`:类型名称,不允许重复,设置默认值为 `default`。
|
||||
- `description`:类型描述,可为空,用于说明该类型的用途。
|
||||
- `created_at`:记录类型创建时间。
|
||||
- **配置项表(configs)**:
|
||||
- `config_id`:自增主键,唯一标识每个配置项。
|
||||
- `type_id`:外键,关联类型表的 `type_id`。
|
||||
- `config_key`:关键字名称,在同一 `type_id` 下必须唯一。
|
||||
- `config_value`:配置项的值,根据业务需求确定数据类型。
|
||||
- `key_description`:关键字描述,可为空。
|
||||
- `created_at`:记录配置项创建时间。
|
||||
- `updated_at`:记录配置项最后更新时间。
|
||||
- 建立索引:在频繁查询的字段上建立索引,如 `type_id`、`config_key`,以提升查询效率。
|
||||
|
||||
### 4. FastAPI 后端接口设计
|
||||
|
||||
- **异步路由与请求处理**
|
||||
- 充分利用 FastAPI 的异步特性,采用 async/await 语法设计接口,保证高并发请求下的快速响应。
|
||||
- 提供以下主要路由:
|
||||
- `/api/types/`:返回所有类型列表及其描述。
|
||||
- `/api/types/<type_name>/`:根据 `type_name` 返回该类型下所有配置项。
|
||||
- `/api/configs/`:创建新配置项或查询所有配置项(支持多条件查询)。
|
||||
- `/api/configs/<config_id>`:获取、更新或删除指定配置项。
|
||||
- `/api/search/`:支持通过多种条件组合(如 `type`、`key`、`value`)查询配置项。
|
||||
|
||||
## **三、技术选型与开发规范**
|
||||
|
||||
- **后端技术**
|
||||
|
||||
- **框架**:使用 FastAPI,其简洁的语法、强大的类型提示和异步支持完美契合项目需求,可快速构建高性能 API。
|
||||
- **数据库**:选用 SQLite 数据库,对于中小型项目来说,其轻量级、零配置、易上手的特点优势明显。后期可根据实际数据量和性能需求升级为 MySQL 等关系型数据库。
|
||||
- **ORM 工具**:推荐使用 SQLAlchemy,与 FastAPI 配合良好,能高效进行数据库操作和事务管理。
|
||||
- **Websocket(可选)**:若未来需实现配置项实时更新通知等功能,可考虑集成 Websocket。
|
||||
|
||||
- **前端技术**
|
||||
|
||||
- **框架**:建议采用现代 JavaScript 框架,如 React 或 Vue,用于构建交互性强的 Web 界面。React 的组件化思想和虚拟 DOM 技术在复杂界面管理上表现优异;Vue 则以简单易学、轻量级著称,学习成本相对较低。
|
||||
- **UI 框架**:搭配成熟 UI 框架(如 Ant Design 或 Element Plus)使用,可快速搭建具有质感的界面,提升开发效率。其丰富的组件库涵盖表格、表单、输入框、按钮、弹窗等,能够满足前端功能模块开发所需。
|
||||
|
||||
- **开发规范**
|
||||
|
||||
- **代码风格**:严格遵守 PEP 8(Python)、Airbnb(JavaScript)等行业主流代码风格规范,使用工具如 `autopep8`、`prettier` 进行代码格式化和检查。
|
||||
- **文件组织结构**
|
||||
- 项目结构清晰,前后端合理分离。
|
||||
- 后端:
|
||||
```
|
||||
/app
|
||||
/api # API 路由模块
|
||||
/models # 数据库表模型和 ORM 映射
|
||||
/schemas # 请求响应数据模型(Pydantic)
|
||||
main.py # FastAPI 应用入口
|
||||
/config # 配置文件(如数据库连接配置)
|
||||
tests # 单元测试文件
|
||||
requirements.txt # 依赖管理
|
||||
Dockerfile # 容器化部署配置
|
||||
```
|
||||
- 前端:
|
||||
```
|
||||
/frontend
|
||||
/src
|
||||
/components # UI 组件
|
||||
/pages # 页面级组件
|
||||
/api # 调用后端 API 的请求模块
|
||||
/utils # 工具函数
|
||||
public # 静态资源
|
||||
package.json # 依赖管理
|
||||
```
|
||||
- **文档编写**
|
||||
- 项目文档需包含 README.md 文件,内容涵盖项目简介、安装部署步骤、接口说明、使用示例等关键信息。
|
||||
- API 接口文档建议使用 OpenAPI 自动生成,并配合 Swagger UI 或 ReDoc 提供交互式接口说明,方便前后端开发人员快速了解和调试接口。
|
||||
- 关键模块和函数需附加详细注释,采用文档风格注释(如 `docstring`),说明功能、参数、返回值等信息。
|
||||
|
||||
## **四、其他要求**
|
||||
|
||||
- **性能与稳定性**
|
||||
- 采用缓存机制(如 Redis)缓存频繁查询的配置项,减少数据库压力,提升响应速度。
|
||||
- 对重要接口(如增删改查)设置合理请求频率限制,防止恶意攻击或误操作导致系统过载。
|
||||
- 定期对数据库进行备份,并记录日志文件,确保数据安全和问题可追溯。
|
||||
|
||||
- **安全性**
|
||||
- 所有后端接口应进行身份验证和权限控制,限制未授权用户访问敏感配置数据。
|
||||
- 使用 HTTPS 协议加密数据传输,防止配置信息在传输过程中被窃取或篡改。
|
||||
- 对用户输入的数据进行严格的校验和过滤,避免 XSS、CSRF 攻击等安全问题。
|
||||
|
||||
## **五、验证与交付**
|
||||
- **测试计划**
|
||||
- **单元测试**:针对各个功能模块编写单元测试用例,覆盖率达到 80% 以上。
|
||||
- **接口测试**:使用工具(如 Postman、pytest、TestNG)对所有接口进行测试,验证输入输出是否符合预期。
|
||||
- **集成测试**:着重检查前后端交互、数据库操作等跨模块协作功能,确保系统各部分协同工作正常。
|
||||
- **性能测试**:模拟高并发请求场景,测试系统响应时间、吞吐量等性能指标,定位并优化性能瓶颈。
|
||||
|
||||
- **交付内容**
|
||||
- 完整的代码仓库,包括项目源码、依赖文件、配置文件、文档等。
|
||||
- 详细的部署说明文档,描述环境搭建、服务安装配置等步骤。
|
||||
- 实施小范围的灰度发布或 A/B 测试,邀请部分用户提前体验系统的核心功能,收集反馈进行优化调整。
|
||||
|
||||
整个配置中心开发完成后,应具备以下特点:
|
||||
- 提供全面的配置管理功能,满足项目日益增长的配置需求;
|
||||
- 界面操作便捷流畅,提升用户的使用体验与工作效率;
|
||||
- 代码结构清晰规范,便于后期维护与功能扩展;
|
||||
- 系统稳定可靠,具有良好的安全性和可扩展性,为项目的长期稳定运行提供有力支撑。
|
||||
## **修改**
|
||||
1. 精简功能,去除用户管理和鉴权;
|
||||
2. 已完成首页的优化工作,主要改进包括:
|
||||
- 优化了统计卡片的设计,添加了渐变背景和精致的阴影效果
|
||||
- 改进了图标和数字的视觉效果,增加了文字阴影和图标动画
|
||||
- 添加了数据趋势指标,以更直观的方式展示数据状态
|
||||
- 优化了快速入门指南的样式,增加了悬停效果和平滑过渡动画
|
||||
- 调整了响应式布局的间距和尺寸,确保在各种设备上都能良好显示
|
||||
@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from app.api.endpoints import types, configs, users, auth, pages
|
||||
from app.api.endpoints import types, configs, pages
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@ -7,7 +7,5 @@ api_router = APIRouter()
|
||||
api_router.include_router(pages.router, tags=["pages"])
|
||||
|
||||
# API路由,不带前缀(因为前缀在main.py中添加)
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(types.router, prefix="/types", tags=["types"])
|
||||
api_router.include_router(configs.router, prefix="/configs", tags=["configs"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||
api_router.include_router(configs.router, prefix="/configs", tags=["configs"])
|
||||
@ -1,22 +1,12 @@
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from jose import JWTError, jwt
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.config import Config
|
||||
|
||||
# 设置 auto_error=False 使其在没有令牌时不抛出异常
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=False)
|
||||
|
||||
# 修改 is_privilege_mode 函数
|
||||
from app.core.config import settings
|
||||
|
||||
async def is_privilege_mode(db: AsyncSession) -> bool:
|
||||
"""检查是否启用了特权模式"""
|
||||
# 首先检查环境变量中的特权模式设置
|
||||
@ -36,67 +26,20 @@ async def is_privilege_mode(db: AsyncSession) -> bool:
|
||||
print(f"检查特权模式出错: {e}")
|
||||
return False
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
"""创建访问令牌"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256")
|
||||
return encoded_jwt
|
||||
|
||||
async def get_current_user(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
) -> User:
|
||||
"""获取当前用户(必须已登录)"""
|
||||
# 检查特权模式
|
||||
if await is_privilege_mode(db):
|
||||
# 特权模式下,返回管理员用户
|
||||
result = await db.execute(select(User).where(User.role == "admin").limit(1))
|
||||
admin_user = result.scalar_one_or_none()
|
||||
if admin_user:
|
||||
return admin_user
|
||||
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无法验证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if not token:
|
||||
raise credentials_exception
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
result = await db.execute(select(User).where(User.username == username))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
return user
|
||||
|
||||
async def get_current_user_optional(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
) -> Optional[User]:
|
||||
# 以下函数用于兼容性目的,返回None表示无用户
|
||||
async def get_current_user_optional(db: AsyncSession = Depends(get_db)) -> None:
|
||||
"""获取当前用户(可选,未登录返回None)"""
|
||||
# 检查特权模式
|
||||
if await is_privilege_mode(db):
|
||||
# 特权模式下,返回管理员用户
|
||||
result = await db.execute(select(User).where(User.role == "admin").limit(1))
|
||||
admin_user = result.scalar_one_or_none()
|
||||
if admin_user:
|
||||
return admin_user
|
||||
return None
|
||||
|
||||
# 兼容性函数
|
||||
async def get_current_active_user(db: AsyncSession = Depends(get_db)) -> None:
|
||||
"""获取当前活跃用户(兼容性函数)"""
|
||||
return None
|
||||
|
||||
# 兼容性函数
|
||||
async def get_current_admin_user(db: AsyncSession = Depends(get_db)) -> None:
|
||||
"""获取当前管理员用户(兼容性函数)"""
|
||||
return None
|
||||
|
||||
if not token:
|
||||
return None
|
||||
|
||||
@ -1,19 +1,5 @@
|
||||
# 从模块中导出所需的函数和类
|
||||
from app.api.deps.auth import (
|
||||
create_access_token,
|
||||
get_current_user,
|
||||
get_current_user_optional,
|
||||
get_current_active_user,
|
||||
get_current_admin_user,
|
||||
is_privilege_mode
|
||||
)
|
||||
from app.core.config import settings
|
||||
|
||||
# 确保这些函数可以直接从 app.api.deps 导入
|
||||
__all__ = [
|
||||
"create_access_token",
|
||||
"get_current_user",
|
||||
"get_current_user_optional",
|
||||
"get_current_active_user",
|
||||
"get_current_admin_user",
|
||||
"is_privilege_mode"
|
||||
]
|
||||
def is_privilege_mode():
|
||||
"""检查是否处于特权模式"""
|
||||
return settings.PRIVILEGE_MODE
|
||||
@ -1,142 +0,0 @@
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from jose import JWTError, jwt
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.config import Config
|
||||
|
||||
# 设置 auto_error=False 使其在没有令牌时不抛出异常
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=False)
|
||||
|
||||
# 修改 is_privilege_mode 函数
|
||||
from app.core.config import settings
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.models.config import Config
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def is_privilege_mode(db: AsyncSession = None) -> bool:
|
||||
"""检查是否启用了特权模式"""
|
||||
# 首先检查环境变量中的特权模式设置
|
||||
logger.info(f"检查特权模式,环境变量设置为: {settings.PRIVILEGE_MODE}")
|
||||
|
||||
if settings.PRIVILEGE_MODE:
|
||||
logger.info("特权模式已启用(通过环境变量)")
|
||||
return True
|
||||
|
||||
# 如果环境变量中没有启用特权模式,则检查数据库中的配置
|
||||
if db:
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Config).where(
|
||||
Config.key == "privilege_mode"
|
||||
)
|
||||
)
|
||||
config = result.scalar_one_or_none()
|
||||
is_enabled = config is not None and config.value.lower() == "true"
|
||||
logger.info(f"特权模式数据库配置: {is_enabled}")
|
||||
return is_enabled
|
||||
except Exception as e:
|
||||
logger.error(f"检查特权模式出错: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
"""创建访问令牌"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256")
|
||||
return encoded_jwt
|
||||
|
||||
async def get_current_user(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
) -> User:
|
||||
"""获取当前用户(必须已登录)"""
|
||||
# 检查特权模式
|
||||
if await is_privilege_mode(db):
|
||||
# 特权模式下,返回管理员用户
|
||||
result = await db.execute(select(User).where(User.role == "admin").limit(1))
|
||||
admin_user = result.scalar_one_or_none()
|
||||
if admin_user:
|
||||
return admin_user
|
||||
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无法验证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if not token:
|
||||
raise credentials_exception
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
result = await db.execute(select(User).where(User.username == username))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
return user
|
||||
|
||||
async def get_current_user_optional(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
) -> Optional[User]:
|
||||
"""获取当前用户(可选,未登录返回None)"""
|
||||
# 检查特权模式
|
||||
if await is_privilege_mode(db):
|
||||
# 特权模式下,返回管理员用户
|
||||
result = await db.execute(select(User).where(User.role == "admin").limit(1))
|
||||
admin_user = result.scalar_one_or_none()
|
||||
if admin_user:
|
||||
return admin_user
|
||||
|
||||
if not token:
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
return None
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
result = await db.execute(select(User).where(User.username == username))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
return user
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
"""获取当前活跃用户"""
|
||||
return current_user
|
||||
|
||||
async def get_current_admin_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
"""获取当前管理员用户"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="权限不足")
|
||||
return current_user
|
||||
@ -1,190 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Form, Request
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
import bcrypt
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from app.models.database import get_db
|
||||
from app.models.user import User
|
||||
from app.api.deps import create_access_token, is_privilege_mode
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 设置模板目录
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent.parent / "templates"))
|
||||
|
||||
@router.post("/token")
|
||||
async def login_for_access_token(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取访问令牌
|
||||
"""
|
||||
# 打印接收到的用户名,用于调试
|
||||
print(f"接收到的用户名: {form_data.username}")
|
||||
|
||||
# 检查特权模式
|
||||
if await is_privilege_mode(db):
|
||||
# 特权模式下,返回管理员用户的令牌
|
||||
result = await db.execute(select(User).where(User.role == "admin").limit(1))
|
||||
admin_user = result.scalar_one_or_none()
|
||||
if admin_user:
|
||||
# 创建访问令牌
|
||||
access_token_expires = timedelta(minutes=60 * 24) # 24小时
|
||||
access_token = create_access_token(
|
||||
data={"sub": admin_user.username}, expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"username": admin_user.username,
|
||||
"role": admin_user.role
|
||||
}
|
||||
|
||||
# 查询用户
|
||||
result = await db.execute(select(User).where(User.username == form_data.username))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 验证密码
|
||||
is_password_correct = bcrypt.checkpw(
|
||||
form_data.password.encode(),
|
||||
user.hashed_password.encode()
|
||||
)
|
||||
|
||||
if not is_password_correct:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 创建访问令牌
|
||||
access_token_expires = timedelta(minutes=60 * 24) # 24小时
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username}, expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"username": user.username,
|
||||
"role": user.role
|
||||
}
|
||||
|
||||
# 添加一个直接接收表单数据的端点,以防OAuth2PasswordRequestForm不工作
|
||||
@router.post("/login")
|
||||
async def login_direct(
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
直接接收表单数据的登录端点
|
||||
"""
|
||||
# 查询用户
|
||||
result = await db.execute(select(User).where(User.username == username))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误"
|
||||
)
|
||||
|
||||
# 验证密码
|
||||
is_password_correct = bcrypt.checkpw(
|
||||
password.encode(),
|
||||
user.hashed_password.encode()
|
||||
)
|
||||
|
||||
if not is_password_correct:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误"
|
||||
)
|
||||
|
||||
# 创建访问令牌
|
||||
access_token_expires = timedelta(minutes=60 * 24) # 24小时
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username}, expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"username": user.username,
|
||||
"role": user.role
|
||||
}
|
||||
# 添加一个简单的登录处理函数
|
||||
@router.post("/form-login", response_class=HTMLResponse)
|
||||
async def login_form(
|
||||
request: Request,
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""处理表单登录"""
|
||||
try:
|
||||
# 查询用户
|
||||
result = await db.execute(select(User).where(User.username == username))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{"request": request, "error": "用户名或密码错误"}
|
||||
)
|
||||
|
||||
# 验证密码
|
||||
is_password_correct = bcrypt.checkpw(
|
||||
password.encode(),
|
||||
user.hashed_password.encode()
|
||||
)
|
||||
|
||||
if not is_password_correct:
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{"request": request, "error": "用户名或密码错误"}
|
||||
)
|
||||
|
||||
# 创建访问令牌
|
||||
access_token_expires = timedelta(minutes=60 * 24) # 24小时
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username}, expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
# 设置cookie
|
||||
response = RedirectResponse(url="/", status_code=303)
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=f"Bearer {access_token}",
|
||||
httponly=True,
|
||||
max_age=60 * 60 * 24, # 24小时
|
||||
samesite="lax"
|
||||
)
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{"request": request, "error": f"登录失败: {str(e)}"}
|
||||
)
|
||||
# 添加检查特权模式的端点
|
||||
@router.get("/privilege-mode")
|
||||
async def check_privilege_mode(db: AsyncSession = Depends(get_db)):
|
||||
"""检查是否启用了特权模式"""
|
||||
privilege_mode = await is_privilege_mode(db)
|
||||
return {"privilege_mode": privilege_mode}
|
||||
@ -6,8 +6,7 @@ from app.models.database import get_db
|
||||
from app.models.config import Config
|
||||
from app.models.type import Type
|
||||
from app.schemas.config import ConfigCreate, ConfigUpdate, Config as ConfigSchema, ConfigList, ConfigSearch
|
||||
from app.api.deps import get_current_active_user, get_current_admin_user, is_privilege_mode
|
||||
from app.models.user import User
|
||||
from app.api.deps import is_privilege_mode
|
||||
# 添加缺少的导入
|
||||
from app import schemas
|
||||
|
||||
@ -16,8 +15,7 @@ router = APIRouter()
|
||||
@router.post("", response_model=ConfigSchema)
|
||||
async def create_config(
|
||||
config_data: ConfigCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
创建新的配置项
|
||||
@ -178,8 +176,7 @@ async def get_config(
|
||||
async def update_config(
|
||||
config_id: int,
|
||||
config_data: ConfigUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
更新配置项
|
||||
@ -219,8 +216,7 @@ async def update_config(
|
||||
@router.delete("/{config_id}", response_model=dict)
|
||||
async def delete_config(
|
||||
config_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
删除配置项(仅管理员)
|
||||
@ -354,8 +350,7 @@ async def get_config_by_type_and_key(
|
||||
async def delete_config_by_type_and_key(
|
||||
type_name: str,
|
||||
key: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
通过类型名称和键删除配置(仅管理员)
|
||||
@ -387,16 +382,11 @@ async def delete_config_by_type_and_key(
|
||||
async def delete_config(
|
||||
type_name: str,
|
||||
key: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
# current_user: User = Depends(get_current_user_optional)
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
删除配置项
|
||||
"""
|
||||
# 检查特权模式
|
||||
if not current_user and not await is_privilege_mode(db):
|
||||
raise HTTPException(status_code=401, detail="需要认证")
|
||||
|
||||
# 查询配置项
|
||||
result = await db.execute(
|
||||
|
||||
@ -5,8 +5,6 @@ from pathlib import Path
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from app.models.database import get_db
|
||||
from app.api.deps import get_current_user_optional # 修改这里,导入正确的函数
|
||||
from app.models.user import User
|
||||
from app.models.type import Type
|
||||
from app.models.config import Config
|
||||
from typing import Optional
|
||||
@ -39,8 +37,7 @@ async def index(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
async def types_page(
|
||||
request: Request,
|
||||
search: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(get_current_user_optional)
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""配置类型页面"""
|
||||
# 构建查询
|
||||
@ -56,7 +53,6 @@ async def types_page(
|
||||
"types.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"types": types,
|
||||
"search": search
|
||||
}
|
||||
@ -66,63 +62,38 @@ async def types_page(
|
||||
async def configs_page(
|
||||
request: Request,
|
||||
type_name: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(get_current_user_optional)
|
||||
key: Optional[str] = None,
|
||||
value: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""配置项页面"""
|
||||
# 获取所有类型
|
||||
types_result = await db.execute(select(Type))
|
||||
types = types_result.scalars().all()
|
||||
|
||||
# 构建查询
|
||||
query = select(Config)
|
||||
if type_name:
|
||||
# 获取类型ID
|
||||
type_result = await db.execute(select(Type).where(Type.type_name == type_name))
|
||||
selected_type = type_result.scalar_one_or_none()
|
||||
if selected_type:
|
||||
query = query.where(Config.type_id == selected_type.type_id)
|
||||
query = select(Config, Type).join(Type)
|
||||
|
||||
if search:
|
||||
query = query.where(Config.key.contains(search) | Config.value.contains(search) | Config.key_description.contains(search))
|
||||
# 添加筛选条件
|
||||
if type_name:
|
||||
query = query.where(Type.type_name.contains(type_name))
|
||||
if key:
|
||||
query = query.where(Config.key.contains(key))
|
||||
if value:
|
||||
query = query.where(Config.value.contains(value))
|
||||
|
||||
# 执行查询
|
||||
result = await db.execute(query)
|
||||
configs = result.scalars().all()
|
||||
rows = result.all()
|
||||
|
||||
# 获取每个配置的类型信息
|
||||
for config in configs:
|
||||
if not hasattr(config, 'type') or config.type is None:
|
||||
type_result = await db.execute(select(Type).where(Type.type_id == config.type_id))
|
||||
config.type = type_result.scalar_one_or_none()
|
||||
# 查询所有类型(用于筛选)
|
||||
types_result = await db.execute(select(Type))
|
||||
types = types_result.scalars().all()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"configs.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"configs": configs,
|
||||
"configs": rows,
|
||||
"types": types,
|
||||
"selected_type": selected_type if type_name else None,
|
||||
"search": search
|
||||
}
|
||||
)
|
||||
|
||||
@router.get("/login", response_class=HTMLResponse)
|
||||
async def login_page(
|
||||
request: Request,
|
||||
current_user: Optional[User] = Depends(get_current_user_optional)
|
||||
):
|
||||
"""登录页面"""
|
||||
if current_user:
|
||||
# 如果用户已登录,重定向到首页
|
||||
from fastapi.responses import RedirectResponse
|
||||
return RedirectResponse(url="/")
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
{
|
||||
"request": request
|
||||
"type_name": type_name,
|
||||
"key": key,
|
||||
"value": value
|
||||
}
|
||||
)
|
||||
@ -6,8 +6,7 @@ from app.models.database import get_db
|
||||
from app.models.type import Type
|
||||
from app.models.config import Config
|
||||
from app.schemas.type import TypeCreate, TypeUpdate, Type as TypeSchema, TypeList
|
||||
from app.api.deps import get_current_active_user, get_current_admin_user, get_current_user_optional, is_privilege_mode
|
||||
from app.models.user import User
|
||||
from app.api.deps import is_privilege_mode
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -41,15 +40,11 @@ async def get_types(
|
||||
@router.post("", response_model=TypeSchema)
|
||||
async def create_type(
|
||||
type_data: TypeCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_optional)
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
创建新的配置类型
|
||||
"""
|
||||
# 检查特权模式
|
||||
if not current_user and not await is_privilege_mode(db):
|
||||
raise HTTPException(status_code=401, detail="需要认证")
|
||||
|
||||
# 检查类型名是否已存在
|
||||
result = await db.execute(select(Type).where(Type.type_name == type_data.type_name))
|
||||
@ -83,15 +78,11 @@ async def get_type(
|
||||
async def update_type(
|
||||
type_name: str,
|
||||
type_data: TypeUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_optional)
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
更新配置类型
|
||||
"""
|
||||
# 检查特权模式
|
||||
if not current_user and not await is_privilege_mode(db):
|
||||
raise HTTPException(status_code=401, detail="需要认证")
|
||||
|
||||
# 查找类型
|
||||
result = await db.execute(select(Type).where(Type.type_name == type_name))
|
||||
@ -111,15 +102,11 @@ async def update_type(
|
||||
@router.delete("/{type_name}", response_model=TypeSchema)
|
||||
async def delete_type(
|
||||
type_name: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_optional)
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
删除配置类型
|
||||
"""
|
||||
# 检查特权模式
|
||||
if not current_user and not await is_privilege_mode(db):
|
||||
raise HTTPException(status_code=401, detail="需要认证")
|
||||
|
||||
# 查询类型
|
||||
result = await db.execute(
|
||||
|
||||
@ -1,131 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from typing import List, Optional
|
||||
import bcrypt
|
||||
from app.models.database import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserCreate, UserUpdate, User as UserSchema, UserList
|
||||
from app.api.deps import get_current_active_user, get_current_admin_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("", response_model=UserList)
|
||||
async def get_users(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
获取所有用户(仅管理员)
|
||||
"""
|
||||
# 查询总数
|
||||
result = await db.execute(select(func.count()).select_from(User))
|
||||
total = result.scalar()
|
||||
|
||||
# 查询用户列表
|
||||
result = await db.execute(select(User).offset(skip).limit(limit))
|
||||
users = result.scalars().all()
|
||||
|
||||
return {"users": users, "total": total}
|
||||
|
||||
@router.post("", response_model=UserSchema)
|
||||
async def create_user(
|
||||
user_data: UserCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
创建新用户(仅管理员)
|
||||
"""
|
||||
# 检查用户名是否已存在
|
||||
result = await db.execute(select(User).where(User.username == user_data.username))
|
||||
if result.scalars().first():
|
||||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||
|
||||
# 哈希密码
|
||||
hashed_password = bcrypt.hashpw(user_data.password.encode(), bcrypt.gensalt()).decode()
|
||||
|
||||
# 创建新用户
|
||||
db_user = User(
|
||||
username=user_data.username,
|
||||
password=hashed_password,
|
||||
role=user_data.role
|
||||
)
|
||||
db.add(db_user)
|
||||
await db.commit()
|
||||
await db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
@router.get("/me", response_model=UserSchema)
|
||||
async def read_users_me(
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""
|
||||
获取当前登录用户信息
|
||||
"""
|
||||
return current_user
|
||||
|
||||
@router.get("/{user_id}", response_model=UserSchema)
|
||||
async def get_user(
|
||||
user_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
获取指定用户信息(仅管理员)
|
||||
"""
|
||||
result = await db.execute(select(User).where(User.user_id == user_id))
|
||||
user = result.scalars().first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail=f"用户ID {user_id} 不存在")
|
||||
return user
|
||||
|
||||
@router.put("/{user_id}", response_model=UserSchema)
|
||||
async def update_user(
|
||||
user_id: int,
|
||||
user_data: UserUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
更新用户信息(仅管理员)
|
||||
"""
|
||||
result = await db.execute(select(User).where(User.user_id == user_id))
|
||||
user = result.scalars().first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail=f"用户ID {user_id} 不存在")
|
||||
|
||||
# 更新用户信息
|
||||
if user_data.password:
|
||||
user.password = bcrypt.hashpw(user_data.password.encode(), bcrypt.gensalt()).decode()
|
||||
if user_data.role:
|
||||
user.role = user_data.role
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
return user
|
||||
|
||||
@router.delete("/{user_id}", response_model=dict)
|
||||
async def delete_user(
|
||||
user_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
删除用户(仅管理员)
|
||||
"""
|
||||
# 不允许删除自己
|
||||
if user_id == current_user.user_id:
|
||||
raise HTTPException(status_code=400, detail="不能删除当前登录的用户")
|
||||
|
||||
result = await db.execute(select(User).where(User.user_id == user_id))
|
||||
user = result.scalars().first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail=f"用户ID {user_id} 不存在")
|
||||
|
||||
await db.delete(user)
|
||||
await db.commit()
|
||||
|
||||
return {"message": f"用户ID {user_id} 已成功删除"}
|
||||
@ -7,7 +7,6 @@ from sqlalchemy import select, func
|
||||
from app.models.database import get_db
|
||||
from app.models.type import Type
|
||||
from app.models.config import Config
|
||||
from app.api.deps import get_current_user_optional
|
||||
|
||||
# 将 router 改名为 page_router
|
||||
page_router = APIRouter()
|
||||
@ -16,7 +15,7 @@ page_router = APIRouter()
|
||||
templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates")
|
||||
|
||||
@page_router.get("/")
|
||||
async def index_page(request: Request, current_user = Depends(get_current_user_optional), db: AsyncSession = Depends(get_db)):
|
||||
async def index_page(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
"""首页"""
|
||||
# 查询类型数量
|
||||
result = await db.execute(select(func.count()).select_from(Type))
|
||||
@ -27,23 +26,19 @@ async def index_page(request: Request, current_user = Depends(get_current_user_o
|
||||
configs_count = result.scalar()
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{"request": request, "current_user": current_user,
|
||||
{"request": request,
|
||||
"types_count": types_count,
|
||||
"configs_count": configs_count
|
||||
}
|
||||
)
|
||||
|
||||
@page_router.get("/login")
|
||||
async def login_page(request: Request):
|
||||
"""登录页面"""
|
||||
return templates.TemplateResponse("login.html", {"request": request})
|
||||
|
||||
|
||||
@page_router.get("/types")
|
||||
async def types_page(
|
||||
request: Request,
|
||||
search: str = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(get_current_user_optional)
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""配置类型页面"""
|
||||
query = select(Type)
|
||||
@ -57,8 +52,7 @@ async def types_page(
|
||||
"types.html",
|
||||
{
|
||||
"request": request,
|
||||
"types": types,
|
||||
"current_user": current_user
|
||||
"types": types
|
||||
}
|
||||
)
|
||||
|
||||
@ -67,8 +61,7 @@ async def configs_page(
|
||||
request: Request,
|
||||
type_name: str = None,
|
||||
search: str = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(get_current_user_optional)
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""配置项页面"""
|
||||
# 获取所有类型
|
||||
@ -93,7 +86,6 @@ async def configs_page(
|
||||
"request": request,
|
||||
"configs": configs,
|
||||
"types": types,
|
||||
"current_type": type_name,
|
||||
"current_user": current_user
|
||||
"current_type": type_name
|
||||
}
|
||||
)
|
||||
@ -3,9 +3,7 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import text, select, inspect
|
||||
from app.core.config import settings
|
||||
import bcrypt
|
||||
from app.models.base import Base
|
||||
from app.models.user import User
|
||||
from app.models.type import Type
|
||||
|
||||
# SQLite异步URL需要使用aiosqlite
|
||||
@ -14,14 +12,13 @@ DATABASE_URL = settings.DATABASE_URL.replace("sqlite:///", "sqlite+aiosqlite:///
|
||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
# 在创建默认配置类型后添加特权模式配置
|
||||
async def init_db():
|
||||
"""初始化数据库,创建所有表"""
|
||||
try:
|
||||
# 检查表是否存在
|
||||
async with engine.connect() as conn:
|
||||
# 检查users表是否存在
|
||||
result = await conn.execute(text("SELECT name FROM sqlite_master WHERE type='table' AND name='users'"))
|
||||
# 检查types表是否存在
|
||||
result = await conn.execute(text("SELECT name FROM sqlite_master WHERE type='table' AND name='types'"))
|
||||
table_exists = result.scalar() is not None
|
||||
|
||||
if not table_exists:
|
||||
@ -31,38 +28,15 @@ async def init_db():
|
||||
|
||||
# 创建默认数据
|
||||
async with AsyncSessionLocal() as session:
|
||||
# 创建默认管理员用户
|
||||
password = "admin123" # 默认密码
|
||||
salt = bcrypt.gensalt()
|
||||
hashed_password = bcrypt.hashpw(password.encode(), salt)
|
||||
|
||||
admin_user = User(
|
||||
username="admin",
|
||||
hashed_password=hashed_password.decode(),
|
||||
role="admin"
|
||||
)
|
||||
session.add(admin_user)
|
||||
|
||||
# 创建默认配置类型
|
||||
default_type = Type(
|
||||
type_name="default",
|
||||
description="默认配置类型"
|
||||
)
|
||||
session.add(default_type)
|
||||
|
||||
# 添加特权模式配置(默认关闭)
|
||||
from app.models.config import Config
|
||||
privilege_config = Config(
|
||||
type_id=1, # 默认类型ID为1
|
||||
key="privilege_mode",
|
||||
value="true",
|
||||
key_description="特权模式:设置为true时,所有用户无需登录即可使用所有功能"
|
||||
)
|
||||
session.add(privilege_config)
|
||||
|
||||
await session.commit()
|
||||
|
||||
logging.info("数据库初始化成功,创建了默认管理员用户(admin/admin123)和默认配置类型")
|
||||
logging.info("数据库初始化成功,创建了默认配置类型")
|
||||
else:
|
||||
logging.info("数据库表已存在,跳过初始化")
|
||||
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime
|
||||
from datetime import datetime
|
||||
from app.models.base import Base
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
user_id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String, unique=True, index=True)
|
||||
hashed_password = Column(String)
|
||||
role = Column(String, default="user") # admin 或 user
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(user_id={self.user_id}, username='{self.username}', role='{self.role}')>"
|
||||
@ -1,37 +0,0 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
class UserBase(BaseModel):
|
||||
username: str = Field(..., description="用户名")
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str = Field(..., description="密码")
|
||||
role: Optional[str] = Field("user", description="用户角色")
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
password: Optional[str] = Field(None, description="密码")
|
||||
role: Optional[str] = Field(None, description="用户角色")
|
||||
|
||||
class UserInDB(UserBase):
|
||||
user_id: int
|
||||
role: str
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class User(UserInDB):
|
||||
pass
|
||||
|
||||
class UserList(BaseModel):
|
||||
users: List[User]
|
||||
total: int
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
@ -31,46 +31,124 @@ body {
|
||||
}
|
||||
|
||||
/* 导航栏样式 */
|
||||
header {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
nav {
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 2rem;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.brand-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.nav-links li {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
.nav-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
.nav-link:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.nav-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.hamburger {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
background-color: white;
|
||||
position: relative;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.hamburger::before,
|
||||
.hamburger::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
background-color: white;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.hamburger::before {
|
||||
top: -6px;
|
||||
}
|
||||
|
||||
.hamburger::after {
|
||||
bottom: -6px;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.nav-menu {
|
||||
display: none;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: var(--primary-color);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.nav-menu.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-toggle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-toggle.active .hamburger {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.nav-toggle.active .hamburger::before {
|
||||
transform: rotate(45deg) translate(4px, 4px);
|
||||
}
|
||||
|
||||
.nav-toggle.active .hamburger::after {
|
||||
transform: rotate(-45deg) translate(4px, -4px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
main {
|
||||
min-height: calc(100vh - 120px);
|
||||
|
||||
25
app/static/js/nav.js
Normal file
25
app/static/js/nav.js
Normal file
@ -0,0 +1,25 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const navToggle = document.querySelector('.nav-toggle');
|
||||
const navMenu = document.querySelector('.nav-menu');
|
||||
|
||||
navToggle.addEventListener('click', function() {
|
||||
navToggle.classList.toggle('active');
|
||||
navMenu.classList.toggle('active');
|
||||
});
|
||||
|
||||
// 点击导航链接时关闭菜单
|
||||
document.querySelectorAll('.nav-link').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
navToggle.classList.remove('active');
|
||||
navMenu.classList.remove('active');
|
||||
});
|
||||
});
|
||||
|
||||
// 点击页面其他区域时关闭菜单
|
||||
document.addEventListener('click', function(event) {
|
||||
if (!navToggle.contains(event.target) && !navMenu.contains(event.target)) {
|
||||
navToggle.classList.remove('active');
|
||||
navMenu.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -8,23 +8,21 @@
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 - 删除重复的导航栏 -->
|
||||
<header>
|
||||
<nav>
|
||||
|
||||
<ul class="nav-links">
|
||||
<li><a href="/page/">首页</a></li>
|
||||
<li><a href="/page/types">配置类型</a></li>
|
||||
<li><a href="/page/configs">配置项</a></li>
|
||||
</ul>
|
||||
<div class="auth-links">
|
||||
<span id="privilegeMode" class="badge badge-warning" style="display: none;">特权模式</span>
|
||||
<span id="userInfo" style="display: none;">
|
||||
欢迎,<span id="username"></span>
|
||||
<button onclick="logout()" class="btn btn-sm">退出</button>
|
||||
</span>
|
||||
<a href="/page/login" id="loginLink">登录</a>
|
||||
<nav class="navbar">
|
||||
<div class="nav-brand">
|
||||
<a href="/page/" class="brand-link">配置中心</a>
|
||||
</div>
|
||||
<div class="nav-menu">
|
||||
<ul class="nav-links">
|
||||
<li><a href="/page/" class="nav-link">首页</a></li>
|
||||
<li><a href="/page/types" class="nav-link">配置类型</a></li>
|
||||
<li><a href="/page/configs" class="nav-link">配置项</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="nav-toggle">
|
||||
<span class="hamburger"></span>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@ -45,68 +43,7 @@
|
||||
|
||||
<!-- 在 script 标签中添加检查特权模式的代码 -->
|
||||
<script>
|
||||
// 检查登录状态
|
||||
function checkLoginStatus() {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const username = localStorage.getItem('username');
|
||||
|
||||
if (token && username) {
|
||||
// 已登录
|
||||
document.getElementById('userInfo').style.display = 'inline-block';
|
||||
document.getElementById('username').textContent = username;
|
||||
document.getElementById('loginLink').style.display = 'none';
|
||||
|
||||
// 添加令牌到所有API请求
|
||||
addTokenToFetch();
|
||||
} else {
|
||||
// 未登录
|
||||
document.getElementById('userInfo').style.display = 'none';
|
||||
document.getElementById('loginLink').style.display = 'inline-block';
|
||||
|
||||
// 检查特权模式
|
||||
checkPrivilegeMode();
|
||||
}
|
||||
}
|
||||
|
||||
// 检查特权模式
|
||||
async function checkPrivilegeMode() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/privilege-mode');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.privilege_mode) {
|
||||
document.getElementById('privilegeMode').style.display = 'inline-block';
|
||||
console.log('特权模式已启用');
|
||||
} else {
|
||||
document.getElementById('privilegeMode').style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查特权模式失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加令牌到所有fetch请求
|
||||
function addTokenToFetch() {
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function(url, options = {}) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
|
||||
if (token && url.startsWith('/api/')) {
|
||||
options.headers = options.headers || {};
|
||||
options.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return originalFetch(url, options);
|
||||
};
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
function logout() {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('role');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
|
||||
// 显示消息
|
||||
function showMessage(text, type = 'info') {
|
||||
@ -125,10 +62,8 @@
|
||||
return confirm(message);
|
||||
}
|
||||
|
||||
// 页面加载时检查登录状态
|
||||
// 页面加载时处理表单
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkLoginStatus();
|
||||
|
||||
// 处理表单AJAX提交
|
||||
document.querySelectorAll('form[data-submit-ajax]').forEach(form => {
|
||||
form.addEventListener('submit', async function(event) {
|
||||
@ -189,5 +124,6 @@
|
||||
</script>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
<script src="/static/js/nav.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -8,37 +8,303 @@
|
||||
<h1 class="card-title">欢迎使用配置中心</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>配置中心是一个统一管理各类配置的平台,提供便捷的配置创建、修改、删除和查询功能。</p>
|
||||
<p class="welcome-text">配置中心是一个统一管理各类配置的平台,提供便捷的配置创建、修改、删除和查询功能。</p>
|
||||
|
||||
<div style="margin-top: 2rem; display: flex; gap: 2rem;">
|
||||
<div class="card" style="flex: 1;">
|
||||
<h3>配置类型</h3>
|
||||
<p style="font-size: 2rem; margin: 1rem 0; color: var(--primary-color);">
|
||||
{{ types_count|default(0) }}
|
||||
</p>
|
||||
<a href="/api/types" class="btn btn-primary">查看类型</a>
|
||||
<div class="stats-container">
|
||||
<div class="stats-card">
|
||||
<div class="stats-icon">
|
||||
<i class="fas fa-layer-group"></i>
|
||||
</div>
|
||||
<div class="stats-content">
|
||||
<h3>配置类型</h3>
|
||||
<div class="stats-info">
|
||||
<div class="stats-number">{{ types_count|default(0) }}</div>
|
||||
<div class="stats-trend positive">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
<span>活跃</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-action">
|
||||
<a href="/page/types" class="btn btn-primary btn-sm">查看类型</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="flex: 1;">
|
||||
<h3>配置项</h3>
|
||||
<p style="font-size: 2rem; margin: 1rem 0; color: var(--primary-color);">
|
||||
{{ configs_count|default(0) }}
|
||||
</p>
|
||||
<a href="/api/configs" class="btn btn-primary">查看配置</a>
|
||||
<div class="stats-card">
|
||||
<div class="stats-icon">
|
||||
<i class="fas fa-cogs"></i>
|
||||
</div>
|
||||
<div class="stats-content">
|
||||
<h3>配置项</h3>
|
||||
<div class="stats-info">
|
||||
<div class="stats-number">{{ configs_count|default(0) }}</div>
|
||||
<div class="stats-trend positive">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
<span>活跃</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-action">
|
||||
<a href="/page/configs" class="btn btn-primary btn-sm">查看配置</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2rem;">
|
||||
<h2>快速入门</h2>
|
||||
<ul style="margin-left: 1.5rem; margin-top: 0.5rem;">
|
||||
<li>在<a href="/api/types">类型管理</a>中创建新的配置类型</li>
|
||||
<li>在<a href="/api/configs">配置管理</a>中添加配置项</li>
|
||||
<li>使用搜索功能快速查找配置</li>
|
||||
{% if current_user and current_user.role == 'admin' %}
|
||||
<li>在<a href="/api/users">用户管理</a>中管理用户权限</li>
|
||||
{% endif %}
|
||||
<div class="quick-start">
|
||||
<h2><i class="fas fa-rocket"></i> 快速入门</h2>
|
||||
<ul>
|
||||
<li><i class="fas fa-plus-circle"></i> 在<a href="/page/types">类型管理</a>中创建新的配置类型</li>
|
||||
<li><i class="fas fa-edit"></i> 在<a href="/page/configs">配置管理</a>中添加和管理配置项</li>
|
||||
<li><i class="fas fa-search"></i> 使用搜索功能快速查找所需的配置</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||
<style>
|
||||
.welcome-text {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.05);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.stats-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 15px 30px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stats-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1.8rem;
|
||||
transition: all 0.4s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-card:hover .stats-icon {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
.stats-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: rgba(255,255,255,0.1);
|
||||
transform: rotate(45deg);
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
|
||||
.stats-card:hover .stats-icon::after {
|
||||
transform: rotate(45deg) translate(50%, 50%);
|
||||
}
|
||||
|
||||
.stats-icon i {
|
||||
font-size: 2rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-content h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 2.4rem;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
line-height: 1;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stats-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
background: rgba(76,175,80,0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.stats-trend.positive {
|
||||
background: rgba(76,175,80,0.1);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.stats-action {
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
.quick-start {
|
||||
margin-top: 3rem;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem;
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.05);
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quick-start::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(25,118,210,0.05) 0%, rgba(25,118,210,0) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.quick-start h2 {
|
||||
margin: 0 0 1.8rem 0;
|
||||
font-size: 1.5rem;
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.quick-start ul {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.quick-start li {
|
||||
margin: 1.2rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
font-size: 1.1rem;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.quick-start li:hover {
|
||||
transform: translateX(8px);
|
||||
}
|
||||
|
||||
.quick-start li i {
|
||||
color: var(--primary-color);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.quick-start a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quick-start a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--primary-color);
|
||||
transform: scaleX(0);
|
||||
transform-origin: right;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.quick-start a:hover::after {
|
||||
transform: scaleX(1);
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.stats-icon i {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.quick-start {
|
||||
padding: 1.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -1,95 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}登录 - 配置中心{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card" style="max-width: 500px; margin: 0 auto; margin-top: 50px;">
|
||||
<div class="card-header">
|
||||
<h1 class="card-title">用户登录</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="login-error" class="alert alert-danger" style="display: none;"></div>
|
||||
<form id="loginForm">
|
||||
<div class="form-group">
|
||||
<label for="username" class="form-label">用户名</label>
|
||||
<input type="text" id="username" name="username" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password" class="form-label">密码</label>
|
||||
<input type="password" id="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.getElementById('loginForm').addEventListener('submit', async function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// 直接从表单元素获取值
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.getElementById('password');
|
||||
|
||||
if (!usernameInput || !passwordInput) {
|
||||
console.error('找不到用户名或密码输入框');
|
||||
showMessage('登录表单错误', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value;
|
||||
|
||||
// 检查用户名和密码是否为空
|
||||
if (!username || !password) {
|
||||
const errorElement = document.getElementById('login-error');
|
||||
errorElement.textContent = '用户名和密码不能为空';
|
||||
errorElement.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('提交登录,用户名:', username); // 调试输出
|
||||
|
||||
try {
|
||||
// 使用URLSearchParams直接构建表单数据
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('username', username);
|
||||
formData.append('password', password);
|
||||
|
||||
const response = await fetch('/api/auth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData.toString(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || '登录失败');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 保存令牌到本地存储
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
localStorage.setItem('username', data.username);
|
||||
localStorage.setItem('role', data.role);
|
||||
|
||||
// 显示成功消息
|
||||
showMessage('登录成功,正在跳转...', 'success');
|
||||
|
||||
// 重定向到首页
|
||||
setTimeout(() => {
|
||||
window.location.href = '/page';
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
const errorElement = document.getElementById('login-error');
|
||||
errorElement.textContent = error.message;
|
||||
errorElement.style.display = 'block';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -3,9 +3,7 @@ uvicorn==0.23.2
|
||||
sqlalchemy==2.0.20
|
||||
alembic==1.12.0
|
||||
pydantic==2.3.0
|
||||
python-jose==3.3.0
|
||||
passlib==1.7.4
|
||||
python-multipart==0.0.6
|
||||
bcrypt==4.0.1
|
||||
pydantic_settings===2.8.1
|
||||
jinja2==3.1.2
|
||||
aiosqlite==0.19.0
|
||||
aiosqlite==0.19.0
|
||||
python-dotenv==1.0.0
|
||||
154
需求分析.md
Normal file
154
需求分析.md
Normal file
@ -0,0 +1,154 @@
|
||||
# 配置中心开发
|
||||
基于fastapi开发的配置中心API,统一管理各类配置,实现配置的集中化管理与便捷操作,并提供了简单的Web界面。
|
||||
|
||||
## **一、项目背景与目标**
|
||||
|
||||
随着项目不断扩展,各类配置(如环境变量、服务参数、数据库连接等)日益增多且复杂。为实现配置的集中化管理与便捷操作,需开发一款配置中心,使团队成员能够高效地查看、修改、新增和删除配置项,降低因配置分散导致的维护成本和出错风险,提升研发效率与系统稳定性。
|
||||
|
||||
## **二、功能模块与详细需求**
|
||||
|
||||
### 1. 配置项管理
|
||||
|
||||
- **增删改查基础功能**
|
||||
- **创建配置项**:支持手动创建新的配置项,至少包含以下字段:
|
||||
- **类型(type_name)**:必填字段,作为配置项的分类标识。默认值为 `default`,用户可自定义类型,如 `db_config`、`service_settings` 等。同时为类型添加描述,清晰说明该类型的用途。
|
||||
- **关键字(key)**:唯一标识配置项的字段,在同一类型下不允许重复。
|
||||
- **值(value)**:配置项的具体内容,可以是字符串、数字、布尔值或 JSON 格式的复杂数据。
|
||||
- **删除配置项**:提供根据配置项的唯一标识(如 `ID` 或组合键 `type+key`)删除配置的功能。
|
||||
- **修改配置项**:支持对配置项的值、描述等信息进行编辑更新。
|
||||
- **查询配置项**:可通过多种条件组合查询,包括但不限于:
|
||||
- 根据类型(type)查询该类型下的所有配置项。
|
||||
- 根据关键字(key)查询匹配的配置项。
|
||||
- 根据值(value)部分或完全匹配查询。
|
||||
|
||||
- **类型与关键字的优先级与关联关系**
|
||||
- 类型(type)具有高于关键字(key)的优先级。在查询或操作时,优先按类型筛选,再按关键字细化。
|
||||
- 同一类型下的关键字必须唯一。不同类型的配置项可以拥有相同的关键字,但其值可能不同。
|
||||
|
||||
### 2. Web 界面设计
|
||||
|
||||
- **界面整体框架**
|
||||
- 遵循现代 Web 设计理念,界面布局清晰、层次分明。采用响应式设计,适配不同设备和屏幕尺寸。
|
||||
- 提供导航栏,方便在首页、配置列表、类型管理等页面之间快速切换。
|
||||
- 界面色调搭配合理,主色调应体现专业性和科技感,兼顾用户的视觉舒适度。建议采用蓝白灰等商务科技风格。
|
||||
|
||||
- **配置列表展示页面**
|
||||
- **类型选择与切换**:在页面顶部或左侧展示类型选择器,以下拉框、标签列表或树状列表等形式呈现所有已存在的类型。点击选择类型后,页面展示该类型下的所有配置项(key-value)列表。
|
||||
- **配置项展示**:每个配置项以简洁卡片式、表格或列表形式展示,包含以下信息:
|
||||
- 类型(type)
|
||||
- 关键字(key)
|
||||
- 值(value)(若值过长,可提供折叠/展开功能)
|
||||
- 修改时间
|
||||
- 操作按钮(编辑、删除等)
|
||||
- **搜索与筛选功能**:提供便捷的搜索框,支持按类型、关键字、值进行实时模糊查询。搜索结果即时展示,同时提供筛选条件组合(如按类型筛选后,再按关键字或值进一步筛选)。
|
||||
|
||||
- **增删改操作界面**
|
||||
- **新增配置页**:提供表单形式,引导用户填写类型、关键字、值等必要信息,自动生成默认值(如默认类型为 `default`)。
|
||||
- **编辑配置页**:可快速定位到待编辑配置项,在原表单界面预填已有信息,允许用户修改相关内容后保存。
|
||||
- **删除确认提示**:在删除配置项时,弹出确认对话框,明确询问用户是否确定删除,避免误操作导致数据丢失。
|
||||
|
||||
### 3. 数据库设计与存储
|
||||
|
||||
- **SQLite 数据库表结构**
|
||||
- **类型表(types)**:
|
||||
- `type_id`:自增主键,唯一标识每个类型。
|
||||
- `type_name`:类型名称,不允许重复,设置默认值为 `default`。
|
||||
- `description`:类型描述,可为空,用于说明该类型的用途。
|
||||
- `created_at`:记录类型创建时间。
|
||||
- **配置项表(configs)**:
|
||||
- `config_id`:自增主键,唯一标识每个配置项。
|
||||
- `type_id`:外键,关联类型表的 `type_id`。
|
||||
- `config_key`:关键字名称,在同一 `type_id` 下必须唯一。
|
||||
- `config_value`:配置项的值,根据业务需求确定数据类型。
|
||||
- `key_description`:关键字描述,可为空。
|
||||
- `created_at`:记录配置项创建时间。
|
||||
- `updated_at`:记录配置项最后更新时间。
|
||||
- 建立索引:在频繁查询的字段上建立索引,如 `type_id`、`config_key`,以提升查询效率。
|
||||
|
||||
### 4. FastAPI 后端接口设计
|
||||
|
||||
- **异步路由与请求处理**
|
||||
- 充分利用 FastAPI 的异步特性,采用 async/await 语法设计接口,保证高并发请求下的快速响应。
|
||||
- 提供以下主要路由:
|
||||
- `/api/types/`:返回所有类型列表及其描述。
|
||||
- `/api/types/<type_name>/`:根据 `type_name` 返回该类型下所有配置项。
|
||||
- `/api/configs/`:创建新配置项或查询所有配置项(支持多条件查询)。
|
||||
- `/api/configs/<config_id>`:获取、更新或删除指定配置项。
|
||||
- `/api/search/`:支持通过多种条件组合(如 `type`、`key`、`value`)查询配置项。
|
||||
|
||||
## **三、技术选型与开发规范**
|
||||
|
||||
- **后端技术**
|
||||
|
||||
- **框架**:使用 FastAPI,其简洁的语法、强大的类型提示和异步支持完美契合项目需求,可快速构建高性能 API。
|
||||
- **数据库**:选用 SQLite 数据库,对于中小型项目来说,其轻量级、零配置、易上手的特点优势明显。后期可根据实际数据量和性能需求升级为 MySQL 等关系型数据库。
|
||||
- **ORM 工具**:推荐使用 SQLAlchemy,与 FastAPI 配合良好,能高效进行数据库操作和事务管理。
|
||||
- **Websocket(可选)**:若未来需实现配置项实时更新通知等功能,可考虑集成 Websocket。
|
||||
|
||||
- **前端技术**
|
||||
|
||||
- **框架**:建议采用现代 JavaScript 框架,如 React 或 Vue,用于构建交互性强的 Web 界面。React 的组件化思想和虚拟 DOM 技术在复杂界面管理上表现优异;Vue 则以简单易学、轻量级著称,学习成本相对较低。
|
||||
- **UI 框架**:搭配成熟 UI 框架(如 Ant Design 或 Element Plus)使用,可快速搭建具有质感的界面,提升开发效率。其丰富的组件库涵盖表格、表单、输入框、按钮、弹窗等,能够满足前端功能模块开发所需。
|
||||
|
||||
- **开发规范**
|
||||
|
||||
- **代码风格**:严格遵守 PEP 8(Python)、Airbnb(JavaScript)等行业主流代码风格规范,使用工具如 `autopep8`、`prettier` 进行代码格式化和检查。
|
||||
- **文件组织结构**
|
||||
- 项目结构清晰,前后端合理分离。
|
||||
- 后端:
|
||||
```
|
||||
/app
|
||||
/api # API 路由模块
|
||||
/models # 数据库表模型和 ORM 映射
|
||||
/schemas # 请求响应数据模型(Pydantic)
|
||||
main.py # FastAPI 应用入口
|
||||
/config # 配置文件(如数据库连接配置)
|
||||
tests # 单元测试文件
|
||||
requirements.txt # 依赖管理
|
||||
Dockerfile # 容器化部署配置
|
||||
```
|
||||
- 前端:
|
||||
```
|
||||
/frontend
|
||||
/src
|
||||
/components # UI 组件
|
||||
/pages # 页面级组件
|
||||
/api # 调用后端 API 的请求模块
|
||||
/utils # 工具函数
|
||||
public # 静态资源
|
||||
package.json # 依赖管理
|
||||
```
|
||||
- **文档编写**
|
||||
- 项目文档需包含 README.md 文件,内容涵盖项目简介、安装部署步骤、接口说明、使用示例等关键信息。
|
||||
- API 接口文档建议使用 OpenAPI 自动生成,并配合 Swagger UI 或 ReDoc 提供交互式接口说明,方便前后端开发人员快速了解和调试接口。
|
||||
- 关键模块和函数需附加详细注释,采用文档风格注释(如 `docstring`),说明功能、参数、返回值等信息。
|
||||
|
||||
## **四、其他要求**
|
||||
|
||||
- **性能与稳定性**
|
||||
- 采用缓存机制(如 Redis)缓存频繁查询的配置项,减少数据库压力,提升响应速度。
|
||||
- 对重要接口(如增删改查)设置合理请求频率限制,防止恶意攻击或误操作导致系统过载。
|
||||
- 定期对数据库进行备份,并记录日志文件,确保数据安全和问题可追溯。
|
||||
|
||||
- **安全性**
|
||||
- 所有后端接口应进行身份验证和权限控制,限制未授权用户访问敏感配置数据。
|
||||
- 使用 HTTPS 协议加密数据传输,防止配置信息在传输过程中被窃取或篡改。
|
||||
- 对用户输入的数据进行严格的校验和过滤,避免 XSS、CSRF 攻击等安全问题。
|
||||
|
||||
## **五、验证与交付**
|
||||
- **测试计划**
|
||||
- **单元测试**:针对各个功能模块编写单元测试用例,覆盖率达到 80% 以上。
|
||||
- **接口测试**:使用工具(如 Postman、pytest、TestNG)对所有接口进行测试,验证输入输出是否符合预期。
|
||||
- **集成测试**:着重检查前后端交互、数据库操作等跨模块协作功能,确保系统各部分协同工作正常。
|
||||
- **性能测试**:模拟高并发请求场景,测试系统响应时间、吞吐量等性能指标,定位并优化性能瓶颈。
|
||||
|
||||
- **交付内容**
|
||||
- 完整的代码仓库,包括项目源码、依赖文件、配置文件、文档等。
|
||||
- 详细的部署说明文档,描述环境搭建、服务安装配置等步骤。
|
||||
- 实施小范围的灰度发布或 A/B 测试,邀请部分用户提前体验系统的核心功能,收集反馈进行优化调整。
|
||||
|
||||
整个配置中心开发完成后,应具备以下特点:
|
||||
- 提供全面的配置管理功能,满足项目日益增长的配置需求;
|
||||
- 界面操作便捷流畅,提升用户的使用体验与工作效率;
|
||||
- 代码结构清晰规范,便于后期维护与功能扩展;
|
||||
- 系统稳定可靠,具有良好的安全性和可扩展性,为项目的长期稳定运行提供有力支撑。
|
||||
Loading…
Reference in New Issue
Block a user