diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3ed95e4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +__pycache__ +*.pyo +*.pyd +.Python +env +venv +.venv +.env +.git +.cache +.pytest_cache +.coverage +htmlcov +*.log +notebooks +.DS_Store +README.md +.dockerignore +Dockerfile \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e90101e..14ab40b 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,60 @@ +# 使用多阶段构建来分离构建环境和运行环境 +FROM python:3.10-slim as builder + +WORKDIR /app + +# 1. 先只复制依赖文件 +COPY requirements.txt . + +# 换源和安装构建依赖 +RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \ + && sed -i 's|security.debian.org/debian-security|mirrors.ustc.edu.cn/debian-security|g' /etc/apt/sources.list.d/debian.sources + +# 安装构建依赖 +RUN apt-get update \ + && apt-get install -y --no-install-recommends gcc python3-dev \ + && rm -rf /var/lib/apt/lists/* + +# 安装Python依赖 +RUN pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple \ + && pip install --user -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple + +# 第二阶段:运行环境 FROM python:3.10-slim WORKDIR /app -COPY . . - +# 换源 RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \ && sed -i 's|security.debian.org/debian-security|mirrors.ustc.edu.cn/debian-security|g' /etc/apt/sources.list.d/debian.sources +# 安装运行时的系统依赖(只安装必要的) RUN apt-get update \ - && apt-get install -y --no-install-recommends gcc python3-dev supervisor \ - && rm -rf /var/lib/apt/lists/* \ - && pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple \ - && pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple \ - && pip install gunicorn wait-for-it -i https://mirrors.aliyun.com/pypi/simple + && apt-get install -y --no-install-recommends supervisor \ + && rm -rf /var/lib/apt/lists/* +# 从构建阶段复制已安装的Python包 +COPY --from=builder /root/.local /root/.local + +# 设置PATH让Python可以找到用户安装的包 +ENV PATH=/root/.local/bin:$PATH +ENV PYTHONPATH=/app + +# 设置时区 ENV TZ="Asia/Shanghai" -RUN mkdir -p /var/log/fastapi_server \ +# 创建目录结构 +RUN mkdir -p /var/log/app_server \ && mkdir -p /var/log/supervisor \ && mkdir -p /etc/supervisor/conf.d +# 复制配置文件 COPY deploy/supervisor.conf /etc/supervisor/supervisord.conf +COPY deploy/app_server.conf /etc/supervisor/conf.d/ -COPY deploy/fastapi_server.conf /etc/supervisor/conf.d/ +# 最后复制应用代码(利用Docker缓存层) +COPY backend/ ./backend EXPOSE 8001 -CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8080"] +CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8080"] \ No newline at end of file diff --git a/backend/app/ai/service/image_text_service.py b/backend/app/ai/service/image_text_service.py index f74b3c7..7e96d14 100644 --- a/backend/app/ai/service/image_text_service.py +++ b/backend/app/ai/service/image_text_service.py @@ -4,8 +4,6 @@ import logging import asyncio from typing import Optional, List -from soupsieve.util import lower - from backend.app.ai.crud import recording_dao from backend.app.ai.crud.image_text_crud import image_text_dao from backend.app.ai.model.image_text import ImageText diff --git a/backend/app/ai/tasks.py b/backend/app/ai/tasks.py index 00a2dc1..716447a 100644 --- a/backend/app/ai/tasks.py +++ b/backend/app/ai/tasks.py @@ -10,9 +10,8 @@ from datetime import datetime from backend.app.ai.crud.image_task_crud import image_task_dao from backend.app.ai.model.image_task import ImageTaskStatus from backend.app.ai.service.image_service import image_service -from backend.database.db import async_db_session, background_db_session -from sqlalchemy.exc import SQLAlchemyError -from asyncpg import SerializationError +from backend.database.db import background_db_session +from sqlalchemy.exc import SQLAlchemyError, OperationalError logger = logging.getLogger(__name__) @@ -85,8 +84,8 @@ async def update_task_status_with_retry(db, task_id, status, result=None, error_ try: result = await image_task_dao.update_task_status(db, task_id, status, result, error_message) return result - except SerializationError as e: - logger.warning(f"Serialization error updating task {task_id} status to {status} (attempt {attempt + 1}): {str(e)}") + except OperationalError as e: + logger.warning(f"Operational error updating task {task_id} status to {status} (attempt {attempt + 1}): {str(e)}") if attempt < max_retries - 1: # Exponential backoff with jitter import random @@ -116,8 +115,8 @@ async def increment_retry_count_with_retry(db, task_id, max_retries=3): try: result = await image_task_dao.increment_retry_count(db, task_id) return result - except SerializationError as e: - logger.warning(f"Serialization error incrementing retry count for task {task_id} (attempt {attempt + 1}): {str(e)}") + except OperationalError as e: + logger.warning(f"Operational error incrementing retry count for task {task_id} (attempt {attempt + 1}): {str(e)}") if attempt < max_retries - 1: # Exponential backoff with jitter import random diff --git a/backend/common/security/jwt.py b/backend/common/security/jwt.py index ac4cec0..4261377 100755 --- a/backend/common/security/jwt.py +++ b/backend/common/security/jwt.py @@ -17,7 +17,9 @@ from pydantic_core import from_json from sqlalchemy.ext.asyncio import AsyncSession from backend.app.admin.model import WxUser -from backend.app.admin.schema.wx import GetWxUserInfoWithRelationDetail +from backend.app.admin.model.dict import DictCategory +from backend.app.admin.schema.wx import GetWxUserInfoWithRelationDetail, DictLevel +from backend.app.admin.service.points_service import points_service from backend.common.dataclasses import AccessToken, NewToken, RefreshToken, TokenPayload from backend.common.exception import errors from backend.common.exception.errors import TokenError @@ -25,6 +27,7 @@ from backend.core.conf import settings from backend.database.db import async_db_session from backend.database.redis import redis_client from backend.utils.serializers import select_as_dict +from backend.utils.snowflake import snowflake from backend.utils.timezone import timezone @@ -337,3 +340,31 @@ async def jwt_authentication(token: str) -> GetWxUserInfoWithRelationDetail: # https://docs.pydantic.dev/latest/concepts/json/#partial-json-parsing user = GetWxUserInfoWithRelationDetail.model_validate(from_json(cache_user, allow_partial=True)) return user + +async def wx_openid_authentication(openid: str, unionid: str) -> GetWxUserInfoWithRelationDetail: + from backend.app.admin.crud.wx_user_crud import wx_user_dao + async with async_db_session() as db: + user = None + try: + # 查找或创建用户 + user = await wx_user_dao.get_by_openid(db, openid) + if not user: + session_key = snowflake.generate() + user = WxUser( + openid=openid, + unionid=unionid, + session_key=session_key, + profile={ + 'dict_level': DictLevel.LEVEL1.value, + 'dict_category': DictCategory.GENERAL.value + }, + ) + await wx_user_dao.add(db, user) + await db.flush() + await db.refresh(user) + await points_service.initialize_user_points(user_id=user.id, db=db) + + return GetWxUserInfoWithRelationDetail(**select_as_dict(user)) + except Exception as e: + db.rollback() + raise diff --git a/backend/middleware/jwt_auth_middleware.py b/backend/middleware/jwt_auth_middleware.py index 43b84ee..1da72e1 100755 --- a/backend/middleware/jwt_auth_middleware.py +++ b/backend/middleware/jwt_auth_middleware.py @@ -10,7 +10,7 @@ from starlette.requests import HTTPConnection from backend.app.admin.schema.wx import GetWxUserInfoWithRelationDetail from backend.common.exception.errors import TokenError from backend.common.log import log -from backend.common.security.jwt import jwt_authentication +from backend.common.security.jwt import jwt_authentication, wx_openid_authentication from backend.core.conf import settings from backend.utils.serializers import MsgSpecJSONResponse @@ -55,6 +55,13 @@ class JwtAuthMiddleware(AuthenticationBackend): :param request: FastAPI 请求对象 :return: """ + wx_openid = request.headers.get('X-WX-OPENID') + wx_unionid = request.headers.get('X-WX-UNIONID') + if wx_openid: + user = await wx_openid_authentication(wx_openid, wx_unionid) + if user: + return AuthCredentials(['authenticated']), user + token = request.headers.get('Authorization') if not token: return None