Files
2026-06-08 18:14:59 +08:00

778 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)