feat(core): 初始化核心配置和部署文件

- 添加 .env.example 环境变量配置示例
- 添加 .gitignore 忽略文件配置
- 添加 core/config.py 配置管理模块
- 添加 deployments/k8s/configmap.yaml Kubernetes 配置
- 添加 core/database.py 数据库连接管理模块
- 添加 core/dependencies.py 全局依赖模块
- 添加 DEPENDENCIES_UPDATED.md 依赖更新记录
- 添加 deployments/k8s/deployment.yaml Kubernetes 部署配置- 添加 deployments/swarm/docker-compose.swarm.yml Docker Swarm 部署配置
- 添加 deployments/docker/docker-compose.yml Docker 部署配置
- 添加 deployments/docker/Dockerfile 应用镜像构建文件
- 添加 middleware/error_handler.py 全局异常处理中间件
This commit is contained in:
danial
2025-11-01 14:32:29 +08:00
commit 0e41e7acce
46 changed files with 7025 additions and 0 deletions

0
apps/__init__.py Normal file
View File

0
apps/app_a/__init__.py Normal file
View File

41
apps/app_a/models.py Normal file
View File

@@ -0,0 +1,41 @@
"""
App A Models - Database models using SQLModel.
"""
from typing import Optional
from datetime import datetime
from sqlmodel import SQLModel, Field
class UserBase(SQLModel):
"""Base user model with shared fields."""
username: str = Field(index=True, max_length=50)
email: str = Field(index=True, max_length=100)
full_name: Optional[str] = Field(default=None, max_length=100)
is_active: bool = Field(default=True)
class User(UserBase, table=True):
"""
User table model.
Represents users in the system.
"""
__tablename__ = "users"
id: Optional[int] = Field(default=None, primary_key=True)
hashed_password: str = Field(max_length=255)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
class Config:
json_schema_extra = {
"example": {
"username": "john_doe",
"email": "john@example.com",
"full_name": "John Doe",
"is_active": True,
}
}

139
apps/app_a/router.py Normal file
View File

@@ -0,0 +1,139 @@
"""
App A Router - API endpoints for user management.
"""
from fastapi import APIRouter, Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession
from core.database import get_session
from core.responses import ApiResponse, success, paginated, ERROR_RESPONSES
from core.dependencies import get_trace_id
from apps.app_a.schemas import UserCreate, UserUpdate, UserResponse
from apps.app_a.services import UserService
router = APIRouter(
prefix="/app-a",
tags=["App A - Users"]
)
@router.post(
"/users",
response_model=ApiResponse[UserResponse],
status_code=201,
summary="Create a new user",
description="Create a new user with username, email, and password",
responses={
201: {"description": "User created successfully"},
400: ERROR_RESPONSES[400],
409: ERROR_RESPONSES[409],
422: ERROR_RESPONSES[422],
500: ERROR_RESPONSES[500],
}
)
async def create_user(
user_data: UserCreate,
session: AsyncSession = Depends(get_session),
trace_id: str = Depends(get_trace_id)
) -> ApiResponse[UserResponse]:
"""Create a new user."""
user = await UserService.create_user(session, user_data)
user_response = UserResponse.model_validate(user)
return success(data=user_response, message="User created successfully", trace_id=trace_id)
@router.get(
"/users/{user_id}",
response_model=ApiResponse[UserResponse],
summary="Get user by ID",
description="Retrieve a user by their ID",
responses={
200: {"description": "User retrieved successfully"},
404: ERROR_RESPONSES[404],
500: ERROR_RESPONSES[500],
}
)
async def get_user(
user_id: int,
session: AsyncSession = Depends(get_session),
trace_id: str = Depends(get_trace_id)
) -> ApiResponse[UserResponse]:
"""Get user by ID."""
user = await UserService.get_user(session, user_id)
user_response = UserResponse.model_validate(user)
return success(data=user_response, trace_id=trace_id)
@router.get(
"/users",
response_model=ApiResponse,
summary="List all users",
description="List all users with pagination support",
responses={
200: {"description": "Users retrieved successfully"},
400: ERROR_RESPONSES[400],
500: ERROR_RESPONSES[500],
}
)
async def list_users(
page: int = 1,
page_size: int = 10,
session: AsyncSession = Depends(get_session),
trace_id: str = Depends(get_trace_id)
) -> ApiResponse:
"""List all users with pagination."""
skip = (page - 1) * page_size
users, total = await UserService.list_users(session, skip, page_size)
user_responses = [UserResponse.model_validate(user) for user in users]
return paginated(
items=user_responses,
total=total,
page=page,
page_size=page_size,
trace_id=trace_id
)
@router.put(
"/users/{user_id}",
response_model=ApiResponse[UserResponse],
summary="Update user",
description="Update user information",
responses={
200: {"description": "User updated successfully"},
400: ERROR_RESPONSES[400],
404: ERROR_RESPONSES[404],
422: ERROR_RESPONSES[422],
500: ERROR_RESPONSES[500],
}
)
async def update_user(
user_id: int,
user_data: UserUpdate,
session: AsyncSession = Depends(get_session),
trace_id: str = Depends(get_trace_id)
) -> ApiResponse[UserResponse]:
"""Update user."""
user = await UserService.update_user(session, user_id, user_data)
user_response = UserResponse.model_validate(user)
return success(data=user_response, message="User updated successfully", trace_id=trace_id)
@router.delete(
"/users/{user_id}",
response_model=ApiResponse[None],
summary="Delete user",
description="Delete a user by ID",
responses={
200: {"description": "User deleted successfully"},
404: ERROR_RESPONSES[404],
500: ERROR_RESPONSES[500],
}
)
async def delete_user(
user_id: int,
session: AsyncSession = Depends(get_session),
trace_id: str = Depends(get_trace_id)
) -> ApiResponse[None]:
"""Delete user."""
await UserService.delete_user(session, user_id)
return success(data=None, message="User deleted successfully", trace_id=trace_id)

69
apps/app_a/schemas.py Normal file
View File

@@ -0,0 +1,69 @@
"""
App A Schemas - Pydantic schemas for request/response validation.
"""
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, Field, EmailStr
class UserCreate(BaseModel):
"""Schema for creating a new user."""
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
password: str = Field(..., min_length=6)
full_name: Optional[str] = Field(None, max_length=100)
class Config:
json_schema_extra = {
"example": {
"username": "john_doe",
"email": "john@example.com",
"password": "secret123",
"full_name": "John Doe",
}
}
class UserUpdate(BaseModel):
"""Schema for updating a user."""
email: Optional[EmailStr] = None
full_name: Optional[str] = Field(None, max_length=100)
is_active: Optional[bool] = None
class Config:
json_schema_extra = {
"example": {
"email": "newemail@example.com",
"full_name": "John Smith",
"is_active": True,
}
}
class UserResponse(BaseModel):
"""Schema for user response."""
id: int
username: str
email: str
full_name: Optional[str]
is_active: bool
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
json_schema_extra = {
"example": {
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"full_name": "John Doe",
"is_active": True,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
}
}

187
apps/app_a/services.py Normal file
View File

@@ -0,0 +1,187 @@
"""
App A Services - Business logic layer.
"""
from typing import Optional
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from apps.app_a.models import User
from apps.app_a.schemas import UserCreate, UserUpdate
from core.exceptions import NotFoundException, AlreadyExistsException
class UserService:
"""Service for user business logic."""
@staticmethod
async def create_user(
session: AsyncSession,
user_data: UserCreate
) -> User:
"""
Create a new user.
Args:
session: Database session
user_data: User creation data
Returns:
User: Created user
Raises:
AlreadyExistsException: If username or email already exists
"""
# Check if username exists
result = await session.execute(
select(User).where(User.username == user_data.username)
)
if result.scalar_one_or_none():
raise AlreadyExistsException(
message=f"Username '{user_data.username}' already exists",
resource="User"
)
# Check if email exists
result = await session.execute(
select(User).where(User.email == user_data.email)
)
if result.scalar_one_or_none():
raise AlreadyExistsException(
message=f"Email '{user_data.email}' already exists",
resource="User"
)
# Create user (in production, hash the password)
user = User(
username=user_data.username,
email=user_data.email,
full_name=user_data.full_name,
hashed_password=f"hashed_{user_data.password}", # TODO: Use proper hashing
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
)
session.add(user)
await session.commit()
await session.refresh(user)
return user
@staticmethod
async def get_user(
session: AsyncSession,
user_id: int
) -> User:
"""
Get user by ID.
Args:
session: Database session
user_id: User ID
Returns:
User: User instance
Raises:
NotFoundException: If user not found
"""
result = await session.execute(
select(User).where(User.id == user_id)
)
user = result.scalar_one_or_none()
if not user:
raise NotFoundException(
message=f"User with ID {user_id} not found",
resource="User"
)
return user
@staticmethod
async def list_users(
session: AsyncSession,
skip: int = 0,
limit: int = 100
) -> tuple[list[User], int]:
"""
List all users with pagination.
Args:
session: Database session
skip: Number of records to skip
limit: Maximum number of records to return
Returns:
tuple: (list of users, total count)
"""
# Get total count
count_result = await session.execute(
select(User)
)
total = len(count_result.all())
# Get paginated results
result = await session.execute(
select(User).offset(skip).limit(limit)
)
users = result.scalars().all()
return list(users), total
@staticmethod
async def update_user(
session: AsyncSession,
user_id: int,
user_data: UserUpdate
) -> User:
"""
Update user.
Args:
session: Database session
user_id: User ID
user_data: Update data
Returns:
User: Updated user
Raises:
NotFoundException: If user not found
"""
user = await UserService.get_user(session, user_id)
# Update fields
if user_data.email is not None:
user.email = user_data.email
if user_data.full_name is not None:
user.full_name = user_data.full_name
if user_data.is_active is not None:
user.is_active = user_data.is_active
user.updated_at = datetime.utcnow()
await session.commit()
await session.refresh(user)
return user
@staticmethod
async def delete_user(
session: AsyncSession,
user_id: int
) -> None:
"""
Delete user.
Args:
session: Database session
user_id: User ID
Raises:
NotFoundException: If user not found
"""
user = await UserService.get_user(session, user_id)
await session.delete(user)
await session.commit()

0
apps/app_b/__init__.py Normal file
View File

45
apps/app_b/models.py Normal file
View File

@@ -0,0 +1,45 @@
"""
App B Models - Database models for products.
"""
from typing import Optional
from datetime import datetime
from decimal import Decimal
from sqlmodel import SQLModel, Field
class ProductBase(SQLModel):
"""Base product model with shared fields."""
name: str = Field(index=True, max_length=100)
description: Optional[str] = Field(default=None, max_length=500)
price: Decimal = Field(decimal_places=2)
stock: int = Field(default=0, ge=0)
is_available: bool = Field(default=True)
class Product(ProductBase, table=True):
"""
Product table model.
Represents products in the system.
"""
__tablename__ = "products"
id: Optional[int] = Field(default=None, primary_key=True)
sku: str = Field(unique=True, index=True, max_length=50)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
class Config:
json_schema_extra = {
"example": {
"name": "Laptop",
"description": "High-performance laptop",
"price": "999.99",
"stock": 10,
"sku": "LAP-001",
"is_available": True,
}
}

93
apps/app_b/router.py Normal file
View File

@@ -0,0 +1,93 @@
"""
App B Router - API endpoints for product management.
"""
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from core.database import get_session
from core.responses import ApiResponse, success, paginated, ERROR_RESPONSES
from core.dependencies import get_trace_id
from apps.app_b.schemas import ProductCreate, ProductResponse
from apps.app_b.services import ProductService
router = APIRouter(
prefix="/app-b",
tags=["App B - Products"]
)
@router.post(
"/products",
response_model=ApiResponse[ProductResponse],
status_code=201,
summary="Create a new product",
description="Create a new product with SKU, name, price, and stock",
responses={
201: {"description": "Product created successfully"},
400: ERROR_RESPONSES[400],
409: ERROR_RESPONSES[409],
422: ERROR_RESPONSES[422],
500: ERROR_RESPONSES[500],
}
)
async def create_product(
product_data: ProductCreate,
session: AsyncSession = Depends(get_session),
trace_id: str = Depends(get_trace_id)
) -> ApiResponse[ProductResponse]:
"""Create a new product."""
product = await ProductService.create_product(session, product_data)
product_response = ProductResponse.model_validate(product)
return success(data=product_response, message="Product created successfully", trace_id=trace_id)
@router.get(
"/products/{product_id}",
response_model=ApiResponse[ProductResponse],
summary="Get product by ID",
description="Retrieve a product by its ID",
responses={
200: {"description": "Product retrieved successfully"},
404: ERROR_RESPONSES[404],
500: ERROR_RESPONSES[500],
}
)
async def get_product(
product_id: int,
session: AsyncSession = Depends(get_session),
trace_id: str = Depends(get_trace_id)
) -> ApiResponse[ProductResponse]:
"""Get product by ID."""
product = await ProductService.get_product(session, product_id)
product_response = ProductResponse.model_validate(product)
return success(data=product_response, trace_id=trace_id)
@router.get(
"/products",
response_model=ApiResponse,
summary="List all products",
description="List all products with pagination support",
responses={
200: {"description": "Products retrieved successfully"},
400: ERROR_RESPONSES[400],
500: ERROR_RESPONSES[500],
}
)
async def list_products(
page: int = 1,
page_size: int = 10,
session: AsyncSession = Depends(get_session),
trace_id: str = Depends(get_trace_id)
) -> ApiResponse:
"""List all products with pagination."""
skip = (page - 1) * page_size
products, total = await ProductService.list_products(session, skip, page_size)
product_responses = [ProductResponse.model_validate(product) for product in products]
return paginated(
items=product_responses,
total=total,
page=page,
page_size=page_size,
trace_id=trace_id
)

80
apps/app_b/schemas.py Normal file
View File

@@ -0,0 +1,80 @@
"""
App B Schemas - Pydantic schemas for products.
"""
from typing import Optional
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel, Field
class ProductCreate(BaseModel):
"""Schema for creating a new product."""
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=500)
price: Decimal = Field(..., gt=0, decimal_places=2)
stock: int = Field(default=0, ge=0)
sku: str = Field(..., min_length=1, max_length=50)
is_available: bool = Field(default=True)
class Config:
json_schema_extra = {
"example": {
"name": "Laptop",
"description": "High-performance laptop",
"price": "999.99",
"stock": 10,
"sku": "LAP-001",
"is_available": True,
}
}
class ProductUpdate(BaseModel):
"""Schema for updating a product."""
name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=500)
price: Optional[Decimal] = Field(None, gt=0, decimal_places=2)
stock: Optional[int] = Field(None, ge=0)
is_available: Optional[bool] = None
class Config:
json_schema_extra = {
"example": {
"name": "Updated Laptop",
"price": "899.99",
"stock": 15,
}
}
class ProductResponse(BaseModel):
"""Schema for product response."""
id: int
name: str
description: Optional[str]
price: Decimal
stock: int
sku: str
is_available: bool
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
json_schema_extra = {
"example": {
"id": 1,
"name": "Laptop",
"description": "High-performance laptop",
"price": "999.99",
"stock": 10,
"sku": "LAP-001",
"is_available": True,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
}
}

78
apps/app_b/services.py Normal file
View File

@@ -0,0 +1,78 @@
"""
App B Services - Business logic for products.
"""
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from apps.app_b.models import Product
from apps.app_b.schemas import ProductCreate, ProductUpdate
from core.exceptions import NotFoundException, AlreadyExistsException
class ProductService:
"""Service for product business logic."""
@staticmethod
async def create_product(
session: AsyncSession,
product_data: ProductCreate
) -> Product:
"""Create a new product."""
# Check if SKU exists
result = await session.execute(
select(Product).where(Product.sku == product_data.sku)
)
if result.scalar_one_or_none():
raise AlreadyExistsException(
message=f"Product with SKU '{product_data.sku}' already exists",
resource="Product"
)
product = Product(
**product_data.model_dump(),
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
)
session.add(product)
await session.commit()
await session.refresh(product)
return product
@staticmethod
async def get_product(
session: AsyncSession,
product_id: int
) -> Product:
"""Get product by ID."""
result = await session.execute(
select(Product).where(Product.id == product_id)
)
product = result.scalar_one_or_none()
if not product:
raise NotFoundException(
message=f"Product with ID {product_id} not found",
resource="Product"
)
return product
@staticmethod
async def list_products(
session: AsyncSession,
skip: int = 0,
limit: int = 100
) -> tuple[list[Product], int]:
"""List all products with pagination."""
count_result = await session.execute(select(Product))
total = len(count_result.all())
result = await session.execute(
select(Product).offset(skip).limit(limit)
)
products = result.scalars().all()
return list(products), total

0
apps/shared/__init__.py Normal file
View File