feat (infra): Trae 完成 asset_helper_backend 微服务基础架构 V1 初始化
核心实现:搭建 Monorepo 架构,完成 shared 共享包、gateway、user-service 基础框架开发 技术落地:严格匹配指定技术栈版本,完成 Docker、gRPC、FastAPI、PostgreSQL、Redis 等配置,实现服务间基础连通 配套文件:生成 Makefile、环境变量模板、数据库 / 脚本初始化文件及启动验证文档 架构定位:仅搭建基础架构骨架,无任何业务逻辑、业务规则及业务相关字段,为后续业务开发提供支撑
This commit is contained in:
19
asset_helper_backend/services/gateway/Dockerfile
Normal file
19
asset_helper_backend/services/gateway/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.13.7-alpine3.22
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app/ .
|
||||
COPY ../../shared/ /shared/
|
||||
|
||||
# 安装共享包
|
||||
RUN pip install -e /shared
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD python -c "import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.connect(('localhost', 8000)); s.close(); print('Healthy')" || exit 1
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
40
asset_helper_backend/services/gateway/app/api/v1/users.py
Normal file
40
asset_helper_backend/services/gateway/app/api/v1/users.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from app.grpc_generated import user_pb2, user_pb2_grpc
|
||||
from app.dependencies import get_user_service_client
|
||||
from typing import List
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/users/{user_id}")
|
||||
async def get_user(user_id: int, client: user_pb2_grpc.UserServiceStub = Depends(get_user_service_client)):
|
||||
request = user_pb2.GetUserRequest(id=user_id)
|
||||
response = await client.GetUser(request)
|
||||
if not response.user.id:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return response.user
|
||||
|
||||
@router.post("/users")
|
||||
async def create_user(username: str, password_hash: str, client: user_pb2_grpc.UserServiceStub = Depends(get_user_service_client)):
|
||||
request = user_pb2.CreateUserRequest(username=username, password_hash=password_hash)
|
||||
response = await client.CreateUser(request)
|
||||
return response.user
|
||||
|
||||
@router.put("/users/{user_id}")
|
||||
async def update_user(user_id: int, username: str, password_hash: str, client: user_pb2_grpc.UserServiceStub = Depends(get_user_service_client)):
|
||||
request = user_pb2.UpdateUserRequest(id=user_id, username=username, password_hash=password_hash)
|
||||
response = await client.UpdateUser(request)
|
||||
if not response.user.id:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return response.user
|
||||
|
||||
@router.delete("/users/{user_id}")
|
||||
async def delete_user(user_id: int, client: user_pb2_grpc.UserServiceStub = Depends(get_user_service_client)):
|
||||
request = user_pb2.DeleteUserRequest(id=user_id)
|
||||
await client.DeleteUser(request)
|
||||
return {"message": "User deleted successfully"}
|
||||
|
||||
@router.get("/users")
|
||||
async def list_users(page: int = 1, page_size: int = 10, client: user_pb2_grpc.UserServiceStub = Depends(get_user_service_client)):
|
||||
request = user_pb2.ListUsersRequest(page=page, page_size=page_size)
|
||||
response = await client.ListUsers(request)
|
||||
return {"users": response.users, "total": response.total}
|
||||
7
asset_helper_backend/services/gateway/app/core/config.py
Normal file
7
asset_helper_backend/services/gateway/app/core/config.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from shared.utils.config import Settings
|
||||
|
||||
class GatewaySettings(Settings):
|
||||
# 继承基础配置,可添加服务特定配置
|
||||
service_name: str = "gateway"
|
||||
|
||||
settings = GatewaySettings()
|
||||
@@ -0,0 +1,7 @@
|
||||
from shared.utils.grpc_client import GrpcClient
|
||||
from app.grpc_generated import user_pb2_grpc
|
||||
|
||||
async def get_user_service_client():
|
||||
with GrpcClient("user-service", 50051) as channel:
|
||||
client = user_pb2_grpc.UserServiceStub(channel)
|
||||
yield client
|
||||
42
asset_helper_backend/services/gateway/app/main.py
Normal file
42
asset_helper_backend/services/gateway/app/main.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from fastapi import FastAPI, WebSocket
|
||||
from app.api.v1 import users
|
||||
from app.ws.handlers import websocket_handler
|
||||
from app.core.config import settings
|
||||
from shared.middleware import CorrelationIdMiddleware, LoggingMiddleware, ExceptionMiddleware
|
||||
from loguru import logger
|
||||
|
||||
app = FastAPI(
|
||||
title="Asset Helper Gateway",
|
||||
version="0.1.0",
|
||||
description="Asset Helper Backend Gateway"
|
||||
)
|
||||
|
||||
# 添加中间件
|
||||
app.add_middleware(CorrelationIdMiddleware)
|
||||
app.add_middleware(LoggingMiddleware)
|
||||
app.add_middleware(ExceptionMiddleware)
|
||||
|
||||
# 注册路由
|
||||
app.include_router(users.router, prefix="/api/v1")
|
||||
|
||||
# WebSocket 端点
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await websocket_handler(websocket)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Asset Helper Gateway is running"}
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=settings.http_port,
|
||||
reload=True
|
||||
)
|
||||
22
asset_helper_backend/services/gateway/app/ws/handlers.py
Normal file
22
asset_helper_backend/services/gateway/app/ws/handlers.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
from app.ws.manager import manager
|
||||
import uuid
|
||||
|
||||
async def websocket_handler(websocket: WebSocket):
|
||||
client_id = str(uuid.uuid4())
|
||||
await manager.connect(websocket, client_id)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive()
|
||||
|
||||
if "text" in data:
|
||||
message = data["text"]
|
||||
await manager.send_personal_message(f"You said: {message}", client_id)
|
||||
await manager.broadcast(f"Client {client_id} said: {message}")
|
||||
elif "bytes" in data:
|
||||
# 处理二进制消息
|
||||
await websocket.send_bytes(data["bytes"])
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(client_id)
|
||||
await manager.broadcast(f"Client {client_id} disconnected")
|
||||
24
asset_helper_backend/services/gateway/app/ws/manager.py
Normal file
24
asset_helper_backend/services/gateway/app/ws/manager.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from fastapi import WebSocket
|
||||
from typing import Dict, List
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: Dict[str, WebSocket] = {}
|
||||
|
||||
async def connect(self, websocket: WebSocket, client_id: str):
|
||||
await websocket.accept()
|
||||
self.active_connections[client_id] = websocket
|
||||
|
||||
def disconnect(self, client_id: str):
|
||||
if client_id in self.active_connections:
|
||||
del self.active_connections[client_id]
|
||||
|
||||
async def send_personal_message(self, message: str, client_id: str):
|
||||
if client_id in self.active_connections:
|
||||
await self.active_connections[client_id].send_text(message)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections.values():
|
||||
await connection.send_text(message)
|
||||
|
||||
manager = ConnectionManager()
|
||||
13
asset_helper_backend/services/gateway/requirements.txt
Normal file
13
asset_helper_backend/services/gateway/requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
grpcio==1.59.0
|
||||
grpcio-tools==1.59.0
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
sqlalchemy==2.0.23
|
||||
asyncpg==0.28.0
|
||||
redis==5.0.1
|
||||
python-dotenv==1.0.0
|
||||
loguru==0.7.2
|
||||
passlib==1.7.4
|
||||
bcrypt==4.1.2
|
||||
19
asset_helper_backend/services/user-service/Dockerfile
Normal file
19
asset_helper_backend/services/user-service/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.13.7-alpine3.22
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app/ .
|
||||
COPY ../../shared/ /shared/
|
||||
|
||||
# 安装共享包
|
||||
RUN pip install -e /shared
|
||||
|
||||
EXPOSE 50051
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD python -c "import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.connect(('localhost', 50051)); s.close(); print('Healthy')" || exit 1
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
@@ -0,0 +1,112 @@
|
||||
import grpc
|
||||
from concurrent import futures
|
||||
from app.grpc_generated import user_pb2, user_pb2_grpc
|
||||
from app.db.models import User
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from sqlalchemy import select
|
||||
from datetime import datetime
|
||||
|
||||
class UserService(user_pb2_grpc.UserServiceServicer):
|
||||
async def GetUser(self, request, context):
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(select(User).where(User.id == request.id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
context.set_code(grpc.StatusCode.NOT_FOUND)
|
||||
context.set_details("User not found")
|
||||
return user_pb2.UserResponse()
|
||||
|
||||
return user_pb2.UserResponse(
|
||||
user=user_pb2.User(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
password_hash=user.password_hash,
|
||||
created_at=user.created_at.isoformat(),
|
||||
updated_at=user.updated_at.isoformat()
|
||||
)
|
||||
)
|
||||
|
||||
async def CreateUser(self, request, context):
|
||||
async with AsyncSessionLocal() as session:
|
||||
user = User(
|
||||
username=request.username,
|
||||
password_hash=request.password_hash
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
||||
return user_pb2.UserResponse(
|
||||
user=user_pb2.User(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
password_hash=user.password_hash,
|
||||
created_at=user.created_at.isoformat(),
|
||||
updated_at=user.updated_at.isoformat()
|
||||
)
|
||||
)
|
||||
|
||||
async def UpdateUser(self, request, context):
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(select(User).where(User.id == request.id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
context.set_code(grpc.StatusCode.NOT_FOUND)
|
||||
context.set_details("User not found")
|
||||
return user_pb2.UserResponse()
|
||||
|
||||
user.username = request.username
|
||||
user.password_hash = request.password_hash
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
||||
return user_pb2.UserResponse(
|
||||
user=user_pb2.User(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
password_hash=user.password_hash,
|
||||
created_at=user.created_at.isoformat(),
|
||||
updated_at=user.updated_at.isoformat()
|
||||
)
|
||||
)
|
||||
|
||||
async def DeleteUser(self, request, context):
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(select(User).where(User.id == request.id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
context.set_code(grpc.StatusCode.NOT_FOUND)
|
||||
context.set_details("User not found")
|
||||
return user_pb2.EmptyResponse()
|
||||
|
||||
await session.delete(user)
|
||||
await session.commit()
|
||||
|
||||
return user_pb2.EmptyResponse()
|
||||
|
||||
async def ListUsers(self, request, context):
|
||||
async with AsyncSessionLocal() as session:
|
||||
offset = (request.page - 1) * request.page_size
|
||||
result = await session.execute(
|
||||
select(User).offset(offset).limit(request.page_size)
|
||||
)
|
||||
users = result.scalars().all()
|
||||
|
||||
total_result = await session.execute(select(User))
|
||||
total = len(total_result.scalars().all())
|
||||
|
||||
user_list = [
|
||||
user_pb2.User(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
password_hash=user.password_hash,
|
||||
created_at=user.created_at.isoformat(),
|
||||
updated_at=user.updated_at.isoformat()
|
||||
)
|
||||
for user in users
|
||||
]
|
||||
|
||||
return user_pb2.UsersResponse(
|
||||
users=user_list,
|
||||
total=total
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
from shared.utils.config import Settings
|
||||
|
||||
class UserServiceSettings(Settings):
|
||||
# 继承基础配置,可添加服务特定配置
|
||||
service_name: str = "user-service"
|
||||
|
||||
settings = UserServiceSettings()
|
||||
@@ -0,0 +1,8 @@
|
||||
from shared.models import BaseDBModel
|
||||
from sqlalchemy import Column, String
|
||||
|
||||
class User(BaseDBModel):
|
||||
__tablename__ = "users"
|
||||
|
||||
username = Column(String, unique=True, index=True, nullable=False)
|
||||
password_hash = Column(String, nullable=False)
|
||||
16
asset_helper_backend/services/user-service/app/db/session.py
Normal file
16
asset_helper_backend/services/user-service/app/db/session.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from services.user_service.app.core.config import settings
|
||||
|
||||
# 注意:这里使用 user_db 数据库
|
||||
DATABASE_URL = f"postgresql+asyncpg://user_service:password@postgres:5432/user_db"
|
||||
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async def get_db():
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
29
asset_helper_backend/services/user-service/app/main.py
Normal file
29
asset_helper_backend/services/user-service/app/main.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import grpc
|
||||
from concurrent import futures
|
||||
from app.api.user_service import UserService
|
||||
from app.grpc_generated import user_pb2_grpc
|
||||
from app.core.config import settings
|
||||
from app.db.models import BaseDBModel
|
||||
from app.db.session import engine
|
||||
import asyncio
|
||||
from loguru import logger
|
||||
|
||||
async def init_db():
|
||||
# 创建数据库表
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(BaseDBModel.metadata.create_all)
|
||||
|
||||
async def serve():
|
||||
server = grpc.aio.server(futures.ThreadPoolExecutor(max_workers=10))
|
||||
user_pb2_grpc.add_UserServiceServicer_to_server(UserService(), server)
|
||||
server.add_insecure_port(f"0.0.0.0:{settings.grpc_port}")
|
||||
|
||||
# 初始化数据库
|
||||
await init_db()
|
||||
|
||||
logger.info(f"Starting gRPC server on port {settings.grpc_port}")
|
||||
await server.start()
|
||||
await server.wait_for_termination()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(serve())
|
||||
13
asset_helper_backend/services/user-service/requirements.txt
Normal file
13
asset_helper_backend/services/user-service/requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
grpcio==1.59.0
|
||||
grpcio-tools==1.59.0
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
sqlalchemy==2.0.23
|
||||
asyncpg==0.28.0
|
||||
redis==5.0.1
|
||||
python-dotenv==1.0.0
|
||||
loguru==0.7.2
|
||||
passlib==1.7.4
|
||||
bcrypt==4.1.2
|
||||
Reference in New Issue
Block a user