diff --git a/app/api/v1/admin_oem_branding.py b/app/api/v1/admin_oem_branding.py index 0d32b8f..2739e73 100644 --- a/app/api/v1/admin_oem_branding.py +++ b/app/api/v1/admin_oem_branding.py @@ -17,7 +17,7 @@ async def get_oem_branding( ) -> ApiResponse[OemBrandingOut]: try: oem = await branding_service.get_default_oem_branding( - db, owner_user_id=admin.id + db, oem_id=1,user_id=admin.id ) except branding_service.AdminOemBrandingError as exc: return ApiResponse(ok=False, message=exc.message) @@ -37,7 +37,7 @@ async def update_oem_branding( ) -> ApiResponse[OemBrandingOut]: try: oem = await branding_service.update_default_oem_branding( - db, body, owner_user_id=admin.id + db, body, oem_id=1,user_id=admin.id ) except branding_service.AdminOemBrandingError as exc: return ApiResponse(ok=False, message=exc.message) diff --git a/app/api/v1/oem_desktop_configs.py b/app/api/v1/oem_desktop_configs.py index 390c07a..4e3957e 100644 --- a/app/api/v1/oem_desktop_configs.py +++ b/app/api/v1/oem_desktop_configs.py @@ -1,4 +1,4 @@ -"""管理员 desktop_configs 键值 CRUD。""" +"""OEM 桌面配置 CRUD(仅当前 OEM 数据)。""" from fastapi import APIRouter, Query @@ -11,12 +11,12 @@ from app.schemas.admin_desktop_config import ( from app.schemas.common import ApiResponse, PaginatedData from app.services import oem_desktop_config as config_service -router = APIRouter(prefix="/oem/desktop-configs", tags=["管理-桌面配置"]) +router = APIRouter(prefix="/oem/desktop-configs", tags=["OEM-桌面配置"]) @router.get("", response_model=ApiResponse[PaginatedData[DesktopConfigOut]]) async def list_configs( - _oem: OemUser, + oem: OemUser, db: DbSession, page: int = Query(1, ge=1), page_size: int = Query( @@ -28,6 +28,7 @@ async def list_configs( ) -> ApiResponse[PaginatedData[DesktopConfigOut]]: rows, total = await config_service.list_configs( db, + oem_user=oem, page=page, page_size=page_size, keyword=keyword, @@ -47,12 +48,12 @@ async def list_configs( @router.post("", response_model=ApiResponse[DesktopConfigOut]) async def create_config( body: DesktopConfigCreate, - _oem: OemUser, + oem: OemUser, db: DbSession, ) -> ApiResponse[DesktopConfigOut]: try: - row = await config_service.create_config(db, body) - except config_service.AdminDesktopConfigError as exc: + row = await config_service.create_config(db, body, oem_user=oem) + except config_service.OemDesktopConfigError as exc: return ApiResponse(ok=False, message=exc.message) return ApiResponse( @@ -66,12 +67,12 @@ async def create_config( async def update_config( config_id: int, body: DesktopConfigUpdate, - _oem: OemUser, + oem: OemUser, db: DbSession, ) -> ApiResponse[DesktopConfigOut]: try: - row = await config_service.update_config(db, config_id, body) - except config_service.AdminDesktopConfigError as exc: + row = await config_service.update_config(db, config_id, body, oem_user=oem) + except config_service.OemDesktopConfigError as exc: return ApiResponse(ok=False, message=exc.message) return ApiResponse( @@ -84,12 +85,12 @@ async def update_config( @router.delete("/{config_id}", response_model=ApiResponse[None]) async def delete_config( config_id: int, - _oem: OemUser, + oem: OemUser, db: DbSession, ) -> ApiResponse[None]: try: - await config_service.delete_config(db, config_id) - except config_service.AdminDesktopConfigError as exc: + await config_service.delete_config(db, config_id, oem_user=oem) + except config_service.OemDesktopConfigError as exc: return ApiResponse(ok=False, message=exc.message) return ApiResponse(ok=True, message="配置已删除") diff --git a/app/api/v1/oem_oem_branding.py b/app/api/v1/oem_oem_branding.py index 0a400f4..9819ed6 100644 --- a/app/api/v1/oem_oem_branding.py +++ b/app/api/v1/oem_oem_branding.py @@ -5,7 +5,7 @@ from fastapi import APIRouter from app.dependencies import OemUser, DbSession from app.schemas.admin_oem_branding import OemBrandingOut, OemBrandingUpdate from app.schemas.common import ApiResponse -from app.services import admin_oem_branding as branding_service +from app.services import oem_oem_branding as branding_service router = APIRouter(prefix="/oem/oem-branding", tags=["管理-OEM品牌"]) @@ -17,7 +17,7 @@ async def get_oem_branding( ) -> ApiResponse[OemBrandingOut]: try: oem = await branding_service.get_default_oem_branding( - db, owner_user_id=admin.id + db, oem_id=1,user_id=admin.id ) except branding_service.AdminOemBrandingError as exc: return ApiResponse(ok=False, message=exc.message) @@ -37,7 +37,7 @@ async def update_oem_branding( ) -> ApiResponse[OemBrandingOut]: try: oem = await branding_service.update_default_oem_branding( - db, body, owner_user_id=admin.id + db, body, oem_id=admin.id,user_id=admin.id ) except branding_service.AdminOemBrandingError as exc: return ApiResponse(ok=False, message=exc.message) diff --git a/app/services/admin_oem_branding.py b/app/services/admin_oem_branding.py index 213d16f..2898102 100644 --- a/app/services/admin_oem_branding.py +++ b/app/services/admin_oem_branding.py @@ -18,18 +18,18 @@ def _map_image_error(exc: BrandingImageError) -> AdminOemBrandingError: return AdminOemBrandingError(exc.message) -async def get_or_create_default_oem(db: AsyncSession, *, owner_user_id: int) -> Oem: - oem = await db.get(Oem, DEFAULT_OEM_ID) +async def get_or_create_default_oem(db: AsyncSession, *, oem_id: int,user_id:int) -> Oem: + oem = await db.get(Oem, oem_id) if oem is not None: return oem - owner = await db.get(User, owner_user_id) + owner = await db.get(User, user_id) if owner is None: raise AdminOemBrandingError("关联用户不存在") oem = Oem( id=DEFAULT_OEM_ID, - user_id=owner_user_id, + user_id=user_id, software_name="", ) db.add(oem) @@ -38,17 +38,18 @@ async def get_or_create_default_oem(db: AsyncSession, *, owner_user_id: int) -> return oem -async def get_default_oem_branding(db: AsyncSession, *, owner_user_id: int) -> Oem: - return await get_or_create_default_oem(db, owner_user_id=owner_user_id) +async def get_default_oem_branding(db: AsyncSession, *, oem_id: int,user_id:int) -> Oem: + return await get_or_create_default_oem(db, oem_id=oem_id,user_id=user_id) async def update_default_oem_branding( db: AsyncSession, body: OemBrandingUpdate, *, - owner_user_id: int, + oem_id: int, + user_id: int, ) -> Oem: - oem = await get_or_create_default_oem(db, owner_user_id=owner_user_id) + oem = await get_or_create_default_oem(db, oem_id=oem_id,user_id=user_id) try: logo_path = resolve_image_field( diff --git a/app/services/oem_desktop_config.py b/app/services/oem_desktop_config.py index 3d9b5bb..2f5b10d 100644 --- a/app/services/oem_desktop_config.py +++ b/app/services/oem_desktop_config.py @@ -1,12 +1,201 @@ -"""desktop_configs 表:键值配置。""" +"""OEM 桌面配置 CRUD(按当前 OEM 用户的 oem 记录隔离)。""" -from sqlalchemy import select +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.models.desktopConfig import DesktopConfig +from app.models.oem import Oem +from app.models.user import User +from app.schemas.admin_desktop_config import DesktopConfigCreate, DesktopConfigUpdate + +DEFAULT_PAGE_SIZE = 20 +MAX_PAGE_SIZE = 200 + +# 新 OEM 首次访问时写入的默认桌面配置(oem_id 由 resolve_oem_id 动态填入) +DEFAULT_DESKTOP_CONFIG_SEED: tuple[tuple[str, str], ...] = ( + ("BAILIAN_API_KEY", "sk-fc1ae323e01947dc9e054aad121796aa"), + ("BAILIAN_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"), + ("ASR_MODE", "online"), + ("OSS_ACCESS_KEY_ID", "LTAI5tSUJiy9NTBdX5sQeC9f"), + ("OSS_ACCESS_KEY_SECRET", "H7D1SjRYy94vCXC7nxIAuZQvaNjBx3"), + ("OSS_BUCKET", "jianhualinshi"), + ("OSS_REGION", "oss-cn-beijing"), + ("LLM_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3/chat/completions"), + ("LLM_MODE_ID", "doubao-seed-2-0-mini-260215"), + ("LLM_API_KEY", "ebff7c25-d00f-483d-94bb-8d1c18b51ddd"), + ("VIDEO_GEN_MODE", "online"), + ("INFINITETALK_API_URL", "http://www.baidu.com"), + ("RUNNINGHUB_WORKFLOW_ID", "2013514129943826433"), + ("RUNNINGHUB_API_KEY", "389cc5f561d742fb9a9cf26f37dda0d3"), +) -async def list_all_as_map(db: AsyncSession,oem_id: int) -> dict[str, str]: +class OemDesktopConfigError(Exception): + def __init__(self, message: str) -> None: + self.message = message + super().__init__(message) + + +# 路由层沿用 AdminDesktopConfigError 名称 +AdminDesktopConfigError = OemDesktopConfigError + +# user_id -> desktop_configs.oem_id(进程内缓存,OEM 绑定变更时需 invalidate) +_oem_id_by_user_id: dict[int, int] = {} + + +def invalidate_oem_id_cache(user_id: int | None = None) -> None: + """清除 resolve_oem_id 缓存;user_id 为 None 时清空全部。""" + if user_id is None: + _oem_id_by_user_id.clear() + else: + _oem_id_by_user_id.pop(user_id, None) + + +async def resolve_oem_id(db: AsyncSession, oem_user: User) -> int: + """desktop_configs.oem_id 对应 oem 表主键;无记录时回退为用户 id。""" + user_id = oem_user.id + cached = _oem_id_by_user_id.get(user_id) + if cached is not None: + return cached + + oem_pk = await db.scalar(select(Oem.id).where(Oem.user_id == user_id)) + resolved = int(oem_pk) if oem_pk is not None else user_id + _oem_id_by_user_id[user_id] = resolved + return resolved + + +async def _count_by_oem(db: AsyncSession, oem_id: int) -> int: + stmt = select(func.count()).select_from(DesktopConfig).where(DesktopConfig.oem_id == oem_id) + return await db.scalar(stmt) or 0 + + +async def ensure_default_desktop_configs(db: AsyncSession, oem_id: int) -> None: + """当前 OEM 无任何配置时写入默认键值。""" + if await _count_by_oem(db, oem_id) > 0: + return + for name, value in DEFAULT_DESKTOP_CONFIG_SEED: + db.add(DesktopConfig(name=name, value=value, oem_id=oem_id)) + await db.flush() + + +async def list_all_as_map(db: AsyncSession, oem_id: int) -> dict[str, str]: + await ensure_default_desktop_configs(db, oem_id) result = await db.execute(select(DesktopConfig).where(DesktopConfig.oem_id == oem_id)) rows = result.scalars().all() return {row.name: row.value for row in rows} + + +async def list_configs( + db: AsyncSession, + *, + oem_user: User, + page: int = 1, + page_size: int = DEFAULT_PAGE_SIZE, + keyword: str | None = None, +) -> tuple[list[DesktopConfig], int]: + oem_id = await resolve_oem_id(db, oem_user) + await ensure_default_desktop_configs(db, oem_id) + page_size = min(max(page_size, 1), MAX_PAGE_SIZE) + page = max(page, 1) + offset = (page - 1) * page_size + + filters = [DesktopConfig.oem_id == oem_id] + if keyword: + like = f"%{keyword.strip()}%" + filters.append(DesktopConfig.name.ilike(like)) + + count_stmt = select(func.count()).select_from(DesktopConfig).where(*filters) + list_stmt = ( + select(DesktopConfig) + .where(*filters) + .order_by(DesktopConfig.name.asc()) + .offset(offset) + .limit(page_size) + ) + + total = await db.scalar(count_stmt) or 0 + result = await db.scalars(list_stmt) + return list(result.all()), total + + +async def create_config( + db: AsyncSession, + body: DesktopConfigCreate, + *, + oem_user: User, +) -> DesktopConfig: + oem_id = await resolve_oem_id(db, oem_user) + existing = await db.scalar( + select(DesktopConfig).where( + DesktopConfig.name == body.name, + DesktopConfig.oem_id == oem_id, + ) + ) + if existing is not None: + raise OemDesktopConfigError("配置键名已存在") + + row = DesktopConfig( + name=body.name, + value=body.value, + mark=body.mark, + oem_id=oem_id, + ) + db.add(row) + await db.flush() + await db.refresh(row) + return row + + +async def _get_owned_config( + db: AsyncSession, + config_id: int, + *, + oem_user: User, +) -> DesktopConfig: + oem_id = await resolve_oem_id(db, oem_user) + row = await db.get(DesktopConfig, config_id) + if row is None or row.oem_id != oem_id: + raise OemDesktopConfigError("配置不存在") + return row + + +async def update_config( + db: AsyncSession, + config_id: int, + body: DesktopConfigUpdate, + *, + oem_user: User, +) -> DesktopConfig: + row = await _get_owned_config(db, config_id, oem_user=oem_user) + + if body.name is not None and body.name != row.name: + taken = await db.scalar( + select(DesktopConfig).where( + DesktopConfig.name == body.name, + DesktopConfig.oem_id == row.oem_id, + DesktopConfig.id != config_id, + ) + ) + if taken is not None: + raise OemDesktopConfigError("配置键名已存在") + row.name = body.name + + if body.value is not None: + row.value = body.value + + if body.mark is not None: + row.mark = body.mark + + await db.flush() + await db.refresh(row) + return row + + +async def delete_config( + db: AsyncSession, + config_id: int, + *, + oem_user: User, +) -> None: + row = await _get_owned_config(db, config_id, oem_user=oem_user) + await db.delete(row) diff --git a/app/services/oem_oem_branding.py b/app/services/oem_oem_branding.py new file mode 100644 index 0000000..31a8a8c --- /dev/null +++ b/app/services/oem_oem_branding.py @@ -0,0 +1,77 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.branding_images import BrandingImageError, resolve_image_field +from app.models.oem import Oem +from app.models.user import User +from app.schemas.admin_oem_branding import OemBrandingUpdate + +DEFAULT_OEM_ID = 1 + + +class AdminOemBrandingError(Exception): + def __init__(self, message: str) -> None: + self.message = message + super().__init__(message) + + +def _map_image_error(exc: BrandingImageError) -> AdminOemBrandingError: + return AdminOemBrandingError(exc.message) + + +async def get_or_create_default_oem(db: AsyncSession, *, user_id:int) -> Oem: + oem_id = await db.scalar(select(Oem.id).where(Oem.user_id == user_id)) + oem = await db.get(Oem, oem_id) + if oem is not None: + return oem + + owner = await db.get(User, user_id) + if owner is None: + raise AdminOemBrandingError("关联用户不存在") + + oem = Oem( + id=oem_id, + user_id=user_id, + software_name="", + ) + db.add(oem) + await db.flush() + await db.refresh(oem) + return oem + + +async def get_default_oem_branding(db: AsyncSession, *, oem_id: int,user_id:int) -> Oem: + return await get_or_create_default_oem(db, user_id=user_id) + + +async def update_default_oem_branding( + db: AsyncSession, + body: OemBrandingUpdate, + *, + oem_id: int, + user_id: int, +) -> Oem: + oem = await get_or_create_default_oem(db, user_id=user_id) + + try: + logo_path = resolve_image_field( + body.logo_path, oem.logo_path, file_prefix="logo" + ) + wechat_qrcode = resolve_image_field( + body.wechat_qrcode, oem.wechat_qrcode, file_prefix="wechat_qr" + ) + qq_qrcode = resolve_image_field( + body.qq_qrcode, oem.qq_qrcode, file_prefix="qq_qr" + ) + except BrandingImageError as exc: + raise _map_image_error(exc) from exc + + oem.software_name = body.software_name + oem.logo_path = logo_path + oem.wechat = body.wechat + oem.qq = body.qq + oem.wechat_qrcode = wechat_qrcode + oem.qq_qrcode = qq_qrcode + + await db.flush() + await db.refresh(oem) + return oem