Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
+777
View File
@@ -0,0 +1,777 @@
# ZQ Platform - FastAPI Backend
基于 FastAPI 的现代化异步后端服务,使用 SQLAlchemy 异步 ORM + Alembic 数据库迁移 + PostgreSQL。
## 技术栈
- **框架**: FastAPI 0.115+
- **数据库**: PostgreSQL 16+
- **ORM**: SQLAlchemy 2.0+ (异步)
- **迁移**: Alembic
- **认证**: JWT
- **缓存**: Redis
- **Python**: 3.12+
## 项目结构
```
backend-fastapi/
├── app/ # 核心应用模块
│ ├── config.py # 配置管理
│ ├── database.py # 数据库连接
│ ├── base_model.py # BaseModel 基类
│ ├── base_schema.py # 通用 Schema
│ ├── base_service.py # BaseService 基类
│ ├── redis.py # Redis 缓存
│ └── excel.py # Excel 工具
├── core/ # 核心业务模块
│ ├── user/ # 用户管理
│ ├── role/ # 角色管理
│ ├── menu/ # 菜单管理
│ ├── dept/ # 部门管理
│ ├── permission/ # 权限管理
│ └── ...
├── scheduler/ # 定时任务模块
│ ├── model.py
│ ├── service.py
│ └── tasks.py
├── zq_demo/ # 示例模块
│ ├── demo/
│ └── demo_cache/
├── scripts/ # 工具脚本
│ ├── dumpdata.py # 数据导出
│ └── loaddata.py # 数据导入
├── alembic/ # 数据库迁移
│ ├── versions/
│ └── env.py
├── env/ # 环境配置
│ ├── dev.env
│ ├── uat.env
│ └── prod.env
├── main.py # 应用入口
├── requirements.txt # 依赖列表
└── README.md
```
## 快速开始
### 1. 环境准备
```bash
# 创建虚拟环境
conda create -n zq-fastapi python=3.12
conda activate zq-fastapi
# 或使用 venv
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
```
### 2. 安装依赖
```bash
pip install -r requirements.txt
```
### 3. 配置环境变量
复制环境配置文件:
```bash
cp env/example.env env/dev.env
```
编辑 `env/dev.env`,配置数据库连接:
```env
# 数据库配置
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/dbname
# Redis 配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
# JWT 配置
SECRET_KEY=your-secret-key-here
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
```
### 4. 数据库迁移
```bash
# 首次使用:生成初始迁移
alembic revision --autogenerate -m "init tables"
# 执行迁移
alembic upgrade head
# 导入数据
python scripts/loaddata.py db_init.json
```
### 5. 启动服务
```bash
# 开发模式(自动重载)
python main.py
# 或使用 uvicorn
uvicorn main:app --reload --host 0.0.0.0 --port 8000
```
### 6. 访问 API 文档
- **Swagger UI**: http://localhost:8000/docs
- **ReDoc**: http://localhost:8000/redoc
## 数据库操作
### 迁移命令
```bash
# 查看当前版本
alembic current
# 查看迁移历史
alembic history
# 生成新的迁移文件
alembic revision --autogenerate -m "描述信息"
# 升级到最新版本
alembic upgrade head
# 回滚一个版本
alembic downgrade -1
# 回滚到指定版本
alembic downgrade <revision_id>
```
### 数据导入导出
#### 导出数据(dumpdata.py
```bash
# 导出所有数据到文件
python scripts/dumpdata.py -o db_init.json -f
# 导出指定模块(如 core
python scripts/dumpdata.py core -o core_data.json -f
# 导出到标准输出(不指定 -o 参数)
python scripts/dumpdata.py > data.json
# 导出指定模块到标准输出
python scripts/dumpdata.py core > core_data.json
```
**参数说明:**
- `app_name`(位置参数,可选):指定要导出的应用/模块名称
- 例如:`core``scheduler``zq_demo`
- 不指定则导出所有数据
- `-o, --output`:指定输出文件路径
- 例如:`-o db_init.json`
- 不指定则输出到标准输出(stdout)
- `-f, --force`:强制覆盖已存在的文件
- 如果输出文件已存在且未使用此参数,脚本会报错并退出
- 使用此参数可以强制覆盖现有文件
**示例:**
```bash
# 导出所有数据,如果文件存在则覆盖
python scripts/dumpdata.py -o db_init.json -f
# 导出 core 模块数据,不覆盖已存在文件(文件存在会报错)
python scripts/dumpdata.py core -o core_data.json
# 导出 scheduler 模块数据到标准输出,然后重定向到文件
python scripts/dumpdata.py scheduler > scheduler_data.json
```
#### 导入数据(loaddata.py
```bash
# 导入数据
python scripts/loaddata.py db_init.json
# 导入多个文件
python scripts/loaddata.py core_data.json scheduler_data.json
```
**参数说明:**
- `files`(位置参数,必需):要导入的 JSON 文件路径,可以指定多个文件
## 开发指南
### 新建模块
按照以下步骤创建新的业务模块(以 `example` 为例):
#### 1. 创建模块目录
```bash
mkdir -p core/example
touch core/example/__init__.py
touch core/example/model.py
touch core/example/schema.py
touch core/example/service.py
touch core/example/api.py
```
#### 2. 定义模型 (model.py)
```python
from sqlalchemy import Column, String, Boolean
from app.base_model import BaseModel
class Example(BaseModel):
__tablename__ = "core_example"
name = Column(String(100), nullable=False, comment="名称")
description = Column(String(500), comment="描述")
is_active = Column(Boolean, default=True, comment="是否激活")
```
#### 3. 定义 Schema (schema.py)
```python
from pydantic import BaseModel, ConfigDict
from typing import Optional
from datetime import datetime
class ExampleBase(BaseModel):
name: str
description: Optional[str] = None
is_active: bool = True
class ExampleCreate(ExampleBase):
pass
class ExampleUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
is_active: Optional[bool] = None
class ExampleResponse(ExampleBase):
id: str
sort: int = 0
is_deleted: bool = False
sys_create_datetime: Optional[datetime] = None
sys_update_datetime: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
```
#### 4. 定义服务 (service.py)
```python
from app.base_service import BaseService
from core.example.model import Example
from core.example.schema import ExampleCreate, ExampleUpdate
class ExampleService(BaseService[Example, ExampleCreate, ExampleUpdate]):
model = Example
```
#### 5. 定义 API (api.py)
```python
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.base_schema import PaginatedResponse, ResponseModel
from core.example.schema import ExampleCreate, ExampleUpdate, ExampleResponse
from core.example.service import ExampleService
router = APIRouter(prefix="/example", tags=["示例管理"])
@router.post("", response_model=ExampleResponse, summary="创建")
async def create(data: ExampleCreate, db: AsyncSession = Depends(get_db)):
return await ExampleService.create(db=db, data=data)
@router.get("", response_model=PaginatedResponse[ExampleResponse], summary="获取列表")
async def get_list(
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
db: AsyncSession = Depends(get_db)
):
items, total = await ExampleService.get_list(db, page=page, page_size=page_size)
return PaginatedResponse(items=items, total=total)
@router.get("/{record_id}", response_model=ExampleResponse, summary="获取详情")
async def get_by_id(record_id: str, db: AsyncSession = Depends(get_db)):
result = await ExampleService.get_by_id(db, record_id=record_id)
if not result:
raise HTTPException(status_code=404, detail="记录不存在")
return result
@router.put("/{record_id}", response_model=ExampleResponse, summary="更新")
async def update(record_id: str, data: ExampleUpdate, db: AsyncSession = Depends(get_db)):
result = await ExampleService.update(db, record_id=record_id, data=data)
if not result:
raise HTTPException(status_code=404, detail="记录不存在")
return result
@router.delete("/{record_id}", response_model=ResponseModel, summary="删除")
async def delete(record_id: str, db: AsyncSession = Depends(get_db)):
success = await ExampleService.delete(db, record_id=record_id)
if not success:
raise HTTPException(status_code=404, detail="记录不存在")
return ResponseModel(message="删除成功")
```
#### 6. 注册路由
`core/router.py` 中添加:
```python
from core.example.api import router as example_router
router.include_router(example_router)
```
#### 7. 生成数据库迁移
```bash
alembic revision --autogenerate -m "add example table"
alembic upgrade head
```
## 核心功能
### BaseModel
所有模型继承自 `BaseModel`,自动包含以下字段:
- `id`: UUID 主键
- `sort`: 排序字段
- `is_deleted`: 软删除标记
- `sys_create_datetime`: 创建时间
- `sys_update_datetime`: 更新时间
- `sys_creator_id`: 创建人ID
- `sys_modifier_id`: 修改人ID
### BaseService
提供通用 CRUD 操作:
- `create()`: 创建记录
- `get_by_id()`: 根据ID获取
- `get_list()`: 分页查询
- `update()`: 更新记录
- `delete()`: 删除记录(软删除/硬删除)
- `check_unique()`: 唯一性检查
- `export_to_excel()`: 导出Excel
- `import_from_excel()`: 导入Excel
### 缓存支持
使用 Redis 缓存,继承 `CacheService` 获得缓存功能:
```python
from app.cache_service import CacheService
class ExampleService(CacheService[Example, ExampleCreate, ExampleUpdate]):
model = Example
cache_prefix = "example"
cache_ttl = 3600 # 1小时
```
## 环境配置
项目支持多环境配置:
- `env/dev.env`: 开发环境
- `env/uat.env`: UAT环境
- `env/prod.env`: 生产环境
通过环境变量 `ENV` 切换:
```bash
export ENV=prod # 使用生产环境配置
python main.py
```
## API 规范
### 响应格式
成功响应:
```json
{
"code": 200,
"message": "success",
"data": {...}
}
```
分页响应:
```json
{
"items": [...],
"total": 100
}
```
错误响应:
```json
{
"detail": "错误信息"
}
```
### 路由命名规范
- 使用小写短横线:`/api/core/user-profile`
- 静态路由在前:`/api/core/menu/check/name`
- 动态路由在后:`/api/core/menu/{menu_id}`
## 常见问题
### 1. 迁移文件为空
确保 `alembic/env.py` 中的 `auto_import_models()` 函数正确扫描了所有模型文件。
### 2. 路由重定向 307
检查路由定义,使用 `@router.post("")` 而不是 `@router.post("/")`
### 3. 数据库连接失败
检查 `env/dev.env` 中的 `DATABASE_URL` 配置是否正确。
# WeasyPrint 安装与配置指南
本文档介绍如何在不同操作系统上安装和配置 WeasyPrint 及其依赖。
## 目录
- [macOS](#macos)
- [Linux (Ubuntu/Debian)](#linux-ubuntudebian)
- [Linux (CentOS/RHEL)](#linux-centosrhel)
- [Windows](#windows)
- [验证安装](#验证安装)
- [常见问题](#常见问题)
---
## macOS
### 1. 安装系统依赖
使用 Homebrew 安装所需的系统库:
```bash
brew install pango glib gobject-introspection harfbuzz cairo fontconfig freetype
```
### 2. 安装 Python 依赖
```bash
pip install weasyprint==62.3
```
### 3. 配置环境变量
WeasyPrint 需要能够找到系统库。将以下内容添加到 `~/.zshrc`(如果使用 bash,则添加到 `~/.bash_profile`):
```bash
export DYLD_LIBRARY_PATH="/opt/homebrew/opt/glib/lib:/opt/homebrew/opt/pango/lib:/opt/homebrew/opt/harfbuzz/lib:/opt/homebrew/opt/cairo/lib:/opt/homebrew/opt/fontconfig/lib:/opt/homebrew/opt/freetype/lib:$DYLD_LIBRARY_PATH"
```
**自动添加方法:**
```bash
echo 'export DYLD_LIBRARY_PATH="/opt/homebrew/opt/glib/lib:/opt/homebrew/opt/pango/lib:/opt/homebrew/opt/harfbuzz/lib:/opt/homebrew/opt/cairo/lib:/opt/homebrew/opt/fontconfig/lib:/opt/homebrew/opt/freetype/lib:$DYLD_LIBRARY_PATH"' >> ~/.zshrc
source ~/.zshrc
```
### 4. 重启终端或 IDE
关闭并重新打开终端窗口,或者重启 IDE,使环境变量生效。
---
## Linux (Ubuntu/Debian)
### 1. 安装系统依赖
```bash
sudo apt-get update
sudo apt-get install -y \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libgdk-pixbuf2.0-0 \
libffi-dev \
libcairo2 \
libglib2.0-0 \
libharfbuzz0b \
libfontconfig1 \
libfreetype6
```
### 2. 安装 Python 依赖
```bash
pip install weasyprint==62.3
```
### 3. 配置环境变量(通常不需要)
在 Linux 上,系统库通常已经在标准路径中,不需要额外配置环境变量。
如果遇到库加载问题,可以添加到 `~/.bashrc`
```bash
export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
source ~/.bashrc
```
---
## Linux (CentOS/RHEL)
### 1. 安装系统依赖
```bash
sudo yum install -y \
pango \
pango-devel \
cairo \
cairo-devel \
glib2 \
glib2-devel \
harfbuzz \
harfbuzz-devel \
fontconfig \
fontconfig-devel \
freetype \
freetype-devel \
libffi-devel
```
或者使用 dnfCentOS 8+):
```bash
sudo dnf install -y \
pango \
pango-devel \
cairo \
cairo-devel \
glib2 \
glib2-devel \
harfbuzz \
harfbuzz-devel \
fontconfig \
fontconfig-devel \
freetype \
freetype-devel \
libffi-devel
```
### 2. 安装 Python 依赖
```bash
pip install weasyprint==62.3
```
### 3. 配置环境变量(如果需要)
```bash
export LD_LIBRARY_PATH="/usr/lib64:$LD_LIBRARY_PATH"
echo 'export LD_LIBRARY_PATH="/usr/lib64:$LD_LIBRARY_PATH"' >> ~/.bashrc
source ~/.bashrc
```
---
## Windows
### 方法 1:使用 GTK3 Runtime(推荐)
1. **下载并安装 GTK3 Runtime**
访问 [GTK for Windows Runtime](https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases),下载最新版本的安装程序(例如 `gtk3-runtime-3.24.31-2022-01-04-ts-win64.exe`)。
2. **运行安装程序**
双击安装程序,按照提示完成安装。默认安装路径为 `C:\Program Files\GTK3-Runtime Win64`
3. **添加到系统 PATH**
- 右键点击"此电脑" → "属性" → "高级系统设置" → "环境变量"
- 在"系统变量"中找到 `Path`,点击"编辑"
- 添加以下路径:
```
C:\Program Files\GTK3-Runtime Win64\bin
```
- 点击"确定"保存
4. **安装 Python 依赖**
```cmd
pip install weasyprint==62.3
```
5. **重启命令提示符或 PowerShell**
### 方法 2:使用 MSYS2(开发者推荐)
1. **安装 MSYS2**
访问 [MSYS2 官网](https://www.msys2.org/),下载并安装 MSYS2。
2. **安装依赖包**
打开 MSYS2 终端,运行:
```bash
pacman -S mingw-w64-x86_64-pango mingw-w64-x86_64-cairo mingw-w64-x86_64-glib2
```
3. **添加到系统 PATH**
将 MSYS2 的 bin 目录添加到系统 PATH:
```
C:\msys64\mingw64\bin
```
4. **安装 Python 依赖**
```cmd
pip install weasyprint==62.3
```
---
## 验证安装
运行以下 Python 代码验证 WeasyPrint 是否正确安装:
```python
from weasyprint import HTML
html_content = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: "PingFang SC", "Microsoft YaHei", sans-serif; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>测试中文字体</h1>
<p>这是一个测试文档,用于验证 WeasyPrint 是否正确安装。</p>
<p>Test English text and 中文文本。</p>
</body>
</html>
"""
try:
pdf_bytes = HTML(string=html_content).write_pdf()
with open('test.pdf', 'wb') as f:
f.write(pdf_bytes)
print("✓ WeasyPrint 安装成功!已生成 test.pdf")
except Exception as e:
print(f"✗ WeasyPrint 安装失败:{e}")
```
如果成功,会在当前目录生成 `test.pdf` 文件。
---
## 常见问题
### 1. macOS: `OSError: cannot load library 'libgobject-2.0-0'`
**原因**:环境变量未正确设置。
**解决方案**
- 确保已添加环境变量到 `~/.zshrc`
- 重启终端或运行 `source ~/.zshrc`
- 如果使用 IDE,需要重启 IDE
### 2. Linux: `ImportError: cannot import name 'HTML' from 'weasyprint'`
**原因**:系统依赖未安装。
**解决方案**
```bash
sudo apt-get install -y libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0
```
### 3. Windows: `OSError: no library called "cairo" was found`
**原因**GTK3 Runtime 未安装或未添加到 PATH。
**解决方案**
- 确保已安装 GTK3 Runtime
- 检查 `C:\Program Files\GTK3-Runtime Win64\bin` 是否在系统 PATH 中
- 重启命令提示符
### 4. 中文字体显示为方块
**原因**:系统缺少中文字体。
**解决方案**
**macOS**
```bash
# 系统自带中文字体,通常不需要额外安装
```
**Linux**
```bash
sudo apt-get install fonts-noto-cjk fonts-wqy-zenhei
```
**Windows**
- 确保系统已安装中文字体(如微软雅黑、宋体等)
- Windows 10/11 默认已包含中文字体
### 5. Conda 环境中的问题
如果在 Conda 环境中遇到库加载问题,尝试:
```bash
# 安装 conda-forge 版本
conda install -c conda-forge weasyprint
```
或者确保环境变量在激活 Conda 环境后仍然有效。
---
## 项目启动
配置完成后,启动后端服务:
```bash
cd /path/to/backend-fastapi
python -m uvicorn main:app --reload
```
如果一切正常,服务应该能够成功启动,并且 PDF 预览功能可以正常使用。
---
## 参考链接
- [WeasyPrint 官方文档](https://doc.courtbouillon.org/weasyprint/stable/)
- [WeasyPrint 安装指南](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#installation)
- [WeasyPrint 故障排除](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#troubleshooting)