From aa2fc6d84de45d07980769f94c16a3195065dd96 Mon Sep 17 00:00:00 2001 From: guyue55 <1724246050@qq.com> Date: Tue, 4 Mar 2025 11:28:17 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E7=B2=BE=E7=AE=80=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=8E=BB=E9=99=A4=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=92=8C=E9=89=B4=E6=9D=83=EF=BC=9B=202.=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=A6=96=E9=A1=B5=20=E5=B7=B2=E5=AE=8C=E6=88=90=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E7=9A=84=E4=BC=98=E5=8C=96=E5=B7=A5=E4=BD=9C=EF=BC=8C?= =?UTF-8?q?=E4=B8=BB=E8=A6=81=E6=94=B9=E8=BF=9B=E5=8C=85=E6=8B=AC=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化了统计卡片的设计,添加了渐变背景和精致的阴影效果 - 改进了图标和数字的视觉效果,增加了文字阴影和图标动画 - 添加了数据趋势指标,以更直观的方式展示数据状态 - 优化了快速入门指南的样式,增加了悬停效果和平滑过渡动画 - 调整了响应式布局的间距和尺寸,确保在各种设备上都能良好显示 --- .gitignore | 3 + README.md | 161 ++---------------- app/api/api.py | 6 +- app/api/deps.py | 85 ++-------- app/api/deps/__init__.py | 22 +-- app/api/deps/auth.py | 142 ---------------- app/api/endpoints/auth.py | 190 --------------------- app/api/endpoints/configs.py | 22 +-- app/api/endpoints/pages.py | 69 +++----- app/api/endpoints/types.py | 21 +-- app/api/endpoints/users.py | 131 --------------- app/api/pages.py | 22 +-- app/models/database.py | 32 +--- app/models/user.py | 16 -- app/schemas/user.py | 37 ----- app/static/css/style.css | 110 ++++++++++-- app/static/js/nav.js | 25 +++ app/templates/base.html | 96 ++--------- app/templates/index.html | 312 ++++++++++++++++++++++++++++++++--- app/templates/login.html | 95 ----------- requirements.txt | 8 +- 需求分析.md | 154 +++++++++++++++++ 22 files changed, 659 insertions(+), 1100 deletions(-) delete mode 100644 app/api/deps/auth.py delete mode 100644 app/api/endpoints/auth.py delete mode 100644 app/api/endpoints/users.py delete mode 100644 app/models/user.py delete mode 100644 app/schemas/user.py create mode 100644 app/static/js/nav.js delete mode 100644 app/templates/login.html create mode 100644 需求分析.md diff --git a/.gitignore b/.gitignore index 15201ac..0769e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,6 @@ cython_debug/ # PyPI configuration file .pypirc + +# other file +*.log \ No newline at end of file diff --git a/README.md b/README.md index d470091..3b8b9b9 100644 --- a/README.md +++ b/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` 返回该类型下所有配置项。 - - `/api/configs/`:创建新配置项或查询所有配置项(支持多条件查询)。 - - `/api/configs/`:获取、更新或删除指定配置项。 - - `/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. 已完成首页的优化工作,主要改进包括: +- 优化了统计卡片的设计,添加了渐变背景和精致的阴影效果 +- 改进了图标和数字的视觉效果,增加了文字阴影和图标动画 +- 添加了数据趋势指标,以更直观的方式展示数据状态 +- 优化了快速入门指南的样式,增加了悬停效果和平滑过渡动画 +- 调整了响应式布局的间距和尺寸,确保在各种设备上都能良好显示 \ No newline at end of file diff --git a/app/api/api.py b/app/api/api.py index e0fea82..8a533be 100644 --- a/app/api/api.py +++ b/app/api/api.py @@ -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"]) \ No newline at end of file +api_router.include_router(configs.router, prefix="/configs", tags=["configs"]) \ No newline at end of file diff --git a/app/api/deps.py b/app/api/deps.py index 435e83b..973651a 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -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 diff --git a/app/api/deps/__init__.py b/app/api/deps/__init__.py index acfca2e..5e55a74 100644 --- a/app/api/deps/__init__.py +++ b/app/api/deps/__init__.py @@ -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" -] \ No newline at end of file +def is_privilege_mode(): + """检查是否处于特权模式""" + return settings.PRIVILEGE_MODE \ No newline at end of file diff --git a/app/api/deps/auth.py b/app/api/deps/auth.py deleted file mode 100644 index dbfe46b..0000000 --- a/app/api/deps/auth.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py deleted file mode 100644 index d5338cf..0000000 --- a/app/api/endpoints/auth.py +++ /dev/null @@ -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} \ No newline at end of file diff --git a/app/api/endpoints/configs.py b/app/api/endpoints/configs.py index 49d5195..93da928 100644 --- a/app/api/endpoints/configs.py +++ b/app/api/endpoints/configs.py @@ -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( diff --git a/app/api/endpoints/pages.py b/app/api/endpoints/pages.py index 9e799cc..31d65db 100644 --- a/app/api/endpoints/pages.py +++ b/app/api/endpoints/pages.py @@ -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 } ) \ No newline at end of file diff --git a/app/api/endpoints/types.py b/app/api/endpoints/types.py index a959ee2..ab31afe 100644 --- a/app/api/endpoints/types.py +++ b/app/api/endpoints/types.py @@ -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( diff --git a/app/api/endpoints/users.py b/app/api/endpoints/users.py deleted file mode 100644 index e7f2bf1..0000000 --- a/app/api/endpoints/users.py +++ /dev/null @@ -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} 已成功删除"} \ No newline at end of file diff --git a/app/api/pages.py b/app/api/pages.py index 871250c..9d097f1 100644 --- a/app/api/pages.py +++ b/app/api/pages.py @@ -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 } ) \ No newline at end of file diff --git a/app/models/database.py b/app/models/database.py index 15c430e..5b1e5fa 100644 --- a/app/models/database.py +++ b/app/models/database.py @@ -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("数据库表已存在,跳过初始化") diff --git a/app/models/user.py b/app/models/user.py deleted file mode 100644 index 118aa20..0000000 --- a/app/models/user.py +++ /dev/null @@ -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"" \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py deleted file mode 100644 index 7d25100..0000000 --- a/app/schemas/user.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/static/css/style.css b/app/static/css/style.css index ed4074a..6fed206 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -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); diff --git a/app/static/js/nav.js b/app/static/js/nav.js new file mode 100644 index 0000000..db2ef6f --- /dev/null +++ b/app/static/js/nav.js @@ -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'); + } + }); +}); \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index efa4a2f..1e639a1 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -8,23 +8,21 @@ {% block extra_css %}{% endblock %} -
-
@@ -45,68 +43,7 @@ {% block extra_js %}{% endblock %} + \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index 2f49419..4dd8cf7 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -8,37 +8,303 @@

欢迎使用配置中心

-

配置中心是一个统一管理各类配置的平台,提供便捷的配置创建、修改、删除和查询功能。

+

配置中心是一个统一管理各类配置的平台,提供便捷的配置创建、修改、删除和查询功能。

-
-
-

配置类型

-

- {{ types_count|default(0) }} -

- 查看类型 +
+
+
+ +
+
+

配置类型

+
+
{{ types_count|default(0) }}
+
+ + 活跃 +
+
+ +
-
-

配置项

-

- {{ configs_count|default(0) }} -

- 查看配置 +
+
+ +
+
+

配置项

+
+
{{ configs_count|default(0) }}
+
+ + 活跃 +
+
+ +
-
-

快速入门

-
    -
  • 类型管理中创建新的配置类型
  • -
  • 配置管理中添加配置项
  • -
  • 使用搜索功能快速查找配置
  • - {% if current_user and current_user.role == 'admin' %} -
  • 用户管理中管理用户权限
  • - {% endif %} +
    +

    快速入门

    +
      +
    • 类型管理中创建新的配置类型
    • +
    • 配置管理中添加和管理配置项
    • +
    • 使用搜索功能快速查找所需的配置
+{% endblock %} + +{% block extra_css %} + + {% endblock %} \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html deleted file mode 100644 index 113a917..0000000 --- a/app/templates/login.html +++ /dev/null @@ -1,95 +0,0 @@ -{% extends "base.html" %} - -{% block title %}登录 - 配置中心{% endblock %} - -{% block content %} -
-
-

用户登录

-
-
- -
-
- - -
-
- - -
- -
-
-
-{% endblock %} - -{% block extra_js %} - -{% endblock %} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e2f72bc..cbcc458 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file +aiosqlite==0.19.0 +python-dotenv==1.0.0 \ No newline at end of file diff --git a/需求分析.md b/需求分析.md new file mode 100644 index 0000000..d470091 --- /dev/null +++ b/需求分析.md @@ -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` 返回该类型下所有配置项。 + - `/api/configs/`:创建新配置项或查询所有配置项(支持多条件查询)。 + - `/api/configs/`:获取、更新或删除指定配置项。 + - `/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 测试,邀请部分用户提前体验系统的核心功能,收集反馈进行优化调整。 + +整个配置中心开发完成后,应具备以下特点: +- 提供全面的配置管理功能,满足项目日益增长的配置需求; +- 界面操作便捷流畅,提升用户的使用体验与工作效率; +- 代码结构清晰规范,便于后期维护与功能扩展; +- 系统稳定可靠,具有良好的安全性和可扩展性,为项目的长期稳定运行提供有力支撑。