1. 精简功能,去除用户管理和鉴权;

2. 优化首页
已完成首页的优化工作,主要改进包括:

- 优化了统计卡片的设计,添加了渐变背景和精致的阴影效果
- 改进了图标和数字的视觉效果,增加了文字阴影和图标动画
- 添加了数据趋势指标,以更直观的方式展示数据状态
- 优化了快速入门指南的样式,增加了悬停效果和平滑过渡动画
- 调整了响应式布局的间距和尺寸,确保在各种设备上都能良好显示
This commit is contained in:
古月 2025-03-04 11:28:17 +08:00
parent c469d57fb6
commit aa2fc6d84d
22 changed files with 659 additions and 1100 deletions

3
.gitignore vendored
View File

@ -169,3 +169,6 @@ cython_debug/
# PyPI configuration file
.pypirc
# other file
*.log

161
README.md
View File

@ -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 8Python、AirbnbJavaScript等行业主流代码风格规范使用工具如 `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. 已完成首页的优化工作,主要改进包括:
- 优化了统计卡片的设计,添加了渐变背景和精致的阴影效果
- 改进了图标和数字的视觉效果,增加了文字阴影和图标动画
- 添加了数据趋势指标,以更直观的方式展示数据状态
- 优化了快速入门指南的样式,增加了悬停效果和平滑过渡动画
- 调整了响应式布局的间距和尺寸,确保在各种设备上都能良好显示

View File

@ -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"])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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(

View File

@ -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
}
)

View File

@ -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(

View File

@ -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} 已成功删除"}

View File

@ -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
}
)

View File

@ -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("数据库表已存在,跳过初始化")

View File

@ -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}')>"

View File

@ -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

View File

@ -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
View 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');
}
});
});

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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
View 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 8Python、AirbnbJavaScript等行业主流代码风格规范使用工具如 `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 测试,邀请部分用户提前体验系统的核心功能,收集反馈进行优化调整。
整个配置中心开发完成后,应具备以下特点:
- 提供全面的配置管理功能,满足项目日益增长的配置需求;
- 界面操作便捷流畅,提升用户的使用体验与工作效率;
- 代码结构清晰规范,便于后期维护与功能扩展;
- 系统稳定可靠,具有良好的安全性和可扩展性,为项目的长期稳定运行提供有力支撑。