first commit
This commit is contained in:
474
docs/backend.md
Normal file
474
docs/backend.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# 后端开发指南
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 技术 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| FastAPI | 0.115+ | Web框架 |
|
||||
| SQLAlchemy | 2.0+ | ORM |
|
||||
| Pydantic | 2.10+ | 数据验证 |
|
||||
| Alembic | 1.14+ | 数据库迁移 |
|
||||
| python-jose | 3.3+ | JWT处理 |
|
||||
| passlib | 1.7+ | 密码哈希 |
|
||||
| asyncpg | 0.30+ | PostgreSQL异步驱动 |
|
||||
| uvicorn | 0.32+ | ASGI服务器 |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── api/v1/ # API路由
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── auth.py # 认证接口
|
||||
│ │ ├── staff.py # 员工接口
|
||||
│ │ ├── departments.py # 科室接口
|
||||
│ │ ├── indicators.py # 指标接口
|
||||
│ │ ├── assessments.py # 考核接口
|
||||
│ │ ├── salary.py # 工资接口
|
||||
│ │ └── stats.py # 统计接口
|
||||
│ ├── core/ # 核心模块
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── config.py # 配置管理
|
||||
│ │ ├── database.py # 数据库连接
|
||||
│ │ ├── security.py # 安全工具
|
||||
│ │ └── init_db.py # 数据库初始化
|
||||
│ ├── models/ # ORM模型
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── models.py # 数据表定义
|
||||
│ ├── schemas/ # Pydantic模式
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── schemas.py # 请求/响应模式
|
||||
│ ├── services/ # 业务逻辑
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── staff_service.py
|
||||
│ │ ├── department_service.py
|
||||
│ │ ├── indicator_service.py
|
||||
│ │ ├── assessment_service.py
|
||||
│ │ ├── salary_service.py
|
||||
│ │ └── stats_service.py
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── __init__.py
|
||||
│ └── main.py # 应用入口
|
||||
├── alembic/ # 数据库迁移
|
||||
│ ├── versions/
|
||||
│ └── env.py
|
||||
├── requirements.txt
|
||||
└── .env # 环境配置
|
||||
```
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 导入规范
|
||||
|
||||
```python
|
||||
# 1. 标准库
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
# 2. 第三方库
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
# 3. 本地模块
|
||||
from app.core.database import get_db
|
||||
from app.schemas.schemas import StaffCreate, StaffResponse
|
||||
from app.services.staff_service import StaffService
|
||||
```
|
||||
|
||||
### 命名规范
|
||||
|
||||
| 类型 | 规范 | 示例 |
|
||||
|------|------|------|
|
||||
| 文件 | snake_case | `staff_service.py` |
|
||||
| 类 | PascalCase | `StaffService`, `StaffCreate` |
|
||||
| 函数/方法 | snake_case | `get_staff_list()` |
|
||||
| 变量 | snake_case | `staff_list`, `department_id` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `DATABASE_URL` |
|
||||
| 枚举值 | UPPER_CASE | `ACTIVE`, `SUBMITTED` |
|
||||
|
||||
### 路由层规范
|
||||
|
||||
```python
|
||||
# api/v1/staff.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.schemas.schemas import StaffCreate, StaffUpdate, StaffResponse
|
||||
from app.services.staff_service import StaffService
|
||||
|
||||
router = APIRouter(prefix="/staff", tags=["员工管理"])
|
||||
|
||||
@router.get("", response_model=dict)
|
||||
async def get_staff_list(
|
||||
name: Optional[str] = Query(None, description="姓名"),
|
||||
department_id: Optional[int] = Query(None, description="科室ID"),
|
||||
status: Optional[str] = Query(None, description="状态"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取员工列表"""
|
||||
staff_list, total = await StaffService.get_list(
|
||||
db, name, department_id, status, page, page_size
|
||||
)
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": staff_list,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size
|
||||
}
|
||||
|
||||
@router.post("", response_model=dict)
|
||||
async def create_staff(
|
||||
data: StaffCreate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""创建员工"""
|
||||
# 检查工号是否已存在
|
||||
existing = await StaffService.get_by_employee_id(db, data.employee_id)
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="工号已存在")
|
||||
|
||||
staff = await StaffService.create(db, data)
|
||||
return {"code": 200, "message": "创建成功", "data": staff}
|
||||
|
||||
@router.put("/{staff_id}", response_model=dict)
|
||||
async def update_staff(
|
||||
staff_id: int,
|
||||
data: StaffUpdate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""更新员工"""
|
||||
staff = await StaffService.get_by_id(db, staff_id)
|
||||
if not staff:
|
||||
raise HTTPException(status_code=404, detail="员工不存在")
|
||||
|
||||
updated = await StaffService.update(db, staff, data)
|
||||
return {"code": 200, "message": "更新成功", "data": updated}
|
||||
|
||||
@router.delete("/{staff_id}")
|
||||
async def delete_staff(
|
||||
staff_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""删除员工"""
|
||||
staff = await StaffService.get_by_id(db, staff_id)
|
||||
if not staff:
|
||||
raise HTTPException(status_code=404, detail="员工不存在")
|
||||
|
||||
await StaffService.delete(db, staff)
|
||||
return {"code": 200, "message": "删除成功"}
|
||||
```
|
||||
|
||||
### Service层规范
|
||||
|
||||
```python
|
||||
# services/staff_service.py
|
||||
from typing import Optional, List, Tuple
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, or_
|
||||
|
||||
from app.models.models import Staff, Department
|
||||
from app.schemas.schemas import StaffCreate, StaffUpdate
|
||||
|
||||
|
||||
class StaffService:
|
||||
"""员工服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_list(
|
||||
db: AsyncSession,
|
||||
name: Optional[str] = None,
|
||||
department_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
) -> Tuple[List[Staff], int]:
|
||||
"""获取员工列表"""
|
||||
query = select(Staff).join(Department)
|
||||
|
||||
# 条件过滤
|
||||
if name:
|
||||
query = query.where(Staff.name.ilike(f"%{name}%"))
|
||||
if department_id:
|
||||
query = query.where(Staff.department_id == department_id)
|
||||
if status:
|
||||
query = query.where(Staff.status == status)
|
||||
|
||||
# 统计总数
|
||||
count_query = select(func.count()).select_from(query.subquery())
|
||||
total = await db.scalar(count_query)
|
||||
|
||||
# 分页
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
result = await db.execute(query)
|
||||
staff_list = result.scalars().all()
|
||||
|
||||
return staff_list, total
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(db: AsyncSession, staff_id: int) -> Optional[Staff]:
|
||||
"""根据ID获取员工"""
|
||||
result = await db.execute(
|
||||
select(Staff).where(Staff.id == staff_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_by_employee_id(db: AsyncSession, employee_id: str) -> Optional[Staff]:
|
||||
"""根据工号获取员工"""
|
||||
result = await db.execute(
|
||||
select(Staff).where(Staff.employee_id == employee_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def create(db: AsyncSession, data: StaffCreate) -> Staff:
|
||||
"""创建员工"""
|
||||
staff = Staff(**data.model_dump())
|
||||
db.add(staff)
|
||||
await db.commit()
|
||||
await db.refresh(staff)
|
||||
return staff
|
||||
|
||||
@staticmethod
|
||||
async def update(db: AsyncSession, staff: Staff, data: StaffUpdate) -> Staff:
|
||||
"""更新员工"""
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(staff, key, value)
|
||||
await db.commit()
|
||||
await db.refresh(staff)
|
||||
return staff
|
||||
|
||||
@staticmethod
|
||||
async def delete(db: AsyncSession, staff: Staff) -> None:
|
||||
"""删除员工"""
|
||||
await db.delete(staff)
|
||||
await db.commit()
|
||||
```
|
||||
|
||||
### Pydantic Schema规范
|
||||
|
||||
```python
|
||||
# schemas/schemas.py
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class StaffStatus(str, Enum):
|
||||
ACTIVE = "active"
|
||||
LEAVE = "leave"
|
||||
RESIGNED = "resigned"
|
||||
RETIRED = "retired"
|
||||
|
||||
|
||||
# 基础模式
|
||||
class StaffBase(BaseModel):
|
||||
"""员工基础模式"""
|
||||
employee_id: str = Field(..., max_length=20, description="工号")
|
||||
name: str = Field(..., max_length=50, description="姓名")
|
||||
department_id: int = Field(..., description="所属科室ID")
|
||||
position: str = Field(..., max_length=50, description="职位")
|
||||
title: Optional[str] = Field(None, max_length=50, description="职称")
|
||||
phone: Optional[str] = Field(None, max_length=20, description="电话")
|
||||
email: Optional[str] = Field(None, max_length=100, description="邮箱")
|
||||
base_salary: float = Field(default=0, ge=0, description="基本工资")
|
||||
performance_ratio: float = Field(default=1.0, ge=0, le=5, description="绩效系数")
|
||||
|
||||
|
||||
# 创建模式
|
||||
class StaffCreate(StaffBase):
|
||||
"""创建员工"""
|
||||
status: StaffStatus = Field(default=StaffStatus.ACTIVE)
|
||||
hire_date: Optional[datetime] = None
|
||||
|
||||
|
||||
# 更新模式
|
||||
class StaffUpdate(BaseModel):
|
||||
"""更新员工"""
|
||||
name: Optional[str] = Field(None, max_length=50)
|
||||
department_id: Optional[int] = None
|
||||
position: Optional[str] = Field(None, max_length=50)
|
||||
title: Optional[str] = Field(None, max_length=50)
|
||||
phone: Optional[str] = Field(None, max_length=20)
|
||||
email: Optional[str] = Field(None, max_length=100)
|
||||
base_salary: Optional[float] = Field(None, ge=0)
|
||||
performance_ratio: Optional[float] = Field(None, ge=0, le=5)
|
||||
status: Optional[StaffStatus] = None
|
||||
|
||||
|
||||
# 响应模式
|
||||
class StaffResponse(StaffBase):
|
||||
"""员工响应"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
status: StaffStatus
|
||||
hire_date: Optional[datetime]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
department_name: Optional[str] = None
|
||||
```
|
||||
|
||||
### ORM模型规范
|
||||
|
||||
```python
|
||||
# models/models.py
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import String, Integer, Numeric, Boolean, DateTime, ForeignKey, Enum as SQLEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from enum import Enum
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class StaffStatus(Enum):
|
||||
ACTIVE = "active"
|
||||
LEAVE = "leave"
|
||||
RESIGNED = "resigned"
|
||||
RETIRED = "retired"
|
||||
|
||||
|
||||
class Staff(Base):
|
||||
"""员工信息表"""
|
||||
__tablename__ = "staff"
|
||||
|
||||
# 使用Mapped类型注解
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
employee_id: Mapped[str] = mapped_column(String(20), unique=True, nullable=False, comment="工号")
|
||||
name: Mapped[str] = mapped_column(String(50), nullable=False, comment="姓名")
|
||||
department_id: Mapped[int] = mapped_column(ForeignKey("departments.id"), nullable=False, comment="所属科室")
|
||||
position: Mapped[str] = mapped_column(String(50), nullable=False, comment="职位")
|
||||
title: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="职称")
|
||||
phone: Mapped[Optional[str]] = mapped_column(String(20), nullable=True, comment="联系电话")
|
||||
email: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment="邮箱")
|
||||
base_salary: Mapped[float] = mapped_column(Numeric(10, 2), default=0, comment="基本工资")
|
||||
performance_ratio: Mapped[float] = mapped_column(Numeric(5, 2), default=1.0, comment="绩效系数")
|
||||
status: Mapped[StaffStatus] = mapped_column(SQLEnum(StaffStatus), default=StaffStatus.ACTIVE, comment="状态")
|
||||
hire_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="入职日期")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
|
||||
|
||||
# 关系
|
||||
department: Mapped["Department"] = relationship("Department", back_populates="staff")
|
||||
|
||||
# 索引
|
||||
__table_args__ = (
|
||||
Index("idx_staff_dept", "department_id"),
|
||||
Index("idx_staff_status", "status"),
|
||||
)
|
||||
```
|
||||
|
||||
### 错误处理规范
|
||||
|
||||
```python
|
||||
# 使用HTTPException返回错误
|
||||
from fastapi import HTTPException
|
||||
|
||||
# 404错误
|
||||
if not staff:
|
||||
raise HTTPException(status_code=404, detail="员工不存在")
|
||||
|
||||
# 400错误(业务规则)
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="工号已存在")
|
||||
|
||||
# 403错误(权限)
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="权限不足")
|
||||
```
|
||||
|
||||
## 配置管理
|
||||
|
||||
```python
|
||||
# core/config.py
|
||||
from pydantic_settings import BaseSettings
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""系统配置"""
|
||||
|
||||
# 应用配置
|
||||
APP_NAME: str = "医院绩效考核管理系统"
|
||||
APP_VERSION: str = "1.0.0"
|
||||
DEBUG: bool = True
|
||||
API_PREFIX: str = "/api/v1"
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/hospital_performance"
|
||||
DATABASE_POOL_SIZE: int = 20
|
||||
DATABASE_MAX_OVERFLOW: int = 10
|
||||
|
||||
# JWT配置
|
||||
SECRET_KEY: str = "your-secret-key-change-in-production"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 480 # 8小时
|
||||
|
||||
# 跨域配置
|
||||
CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:5173"]
|
||||
|
||||
# 分页配置
|
||||
DEFAULT_PAGE_SIZE: int = 20
|
||||
MAX_PAGE_SIZE: int = 100
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
```
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
```bash
|
||||
# 创建迁移
|
||||
alembic revision --autogenerate -m "add new table"
|
||||
|
||||
# 执行迁移
|
||||
alembic upgrade head
|
||||
|
||||
# 回滚
|
||||
alembic downgrade -1
|
||||
|
||||
# 查看历史
|
||||
alembic history
|
||||
```
|
||||
|
||||
## 开发命令
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 启动开发服务器
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
|
||||
# 运行测试
|
||||
pytest
|
||||
|
||||
# 运行特定测试
|
||||
pytest tests/test_staff.py -v
|
||||
```
|
||||
|
||||
## API文档
|
||||
|
||||
启动服务后访问:
|
||||
- Swagger UI: http://localhost:8000/api/v1/docs
|
||||
- ReDoc: http://localhost:8000/api/v1/redoc
|
||||
Reference in New Issue
Block a user