This commit is contained in:
fengchuanhn@gmail.com
2026-05-21 17:19:32 +08:00
parent 13c8493e4c
commit 79b657941f
6 changed files with 296 additions and 28 deletions

View File

@@ -17,7 +17,7 @@ async def get_oem_branding(
) -> ApiResponse[OemBrandingOut]: ) -> ApiResponse[OemBrandingOut]:
try: try:
oem = await branding_service.get_default_oem_branding( 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: except branding_service.AdminOemBrandingError as exc:
return ApiResponse(ok=False, message=exc.message) return ApiResponse(ok=False, message=exc.message)
@@ -37,7 +37,7 @@ async def update_oem_branding(
) -> ApiResponse[OemBrandingOut]: ) -> ApiResponse[OemBrandingOut]:
try: try:
oem = await branding_service.update_default_oem_branding( 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: except branding_service.AdminOemBrandingError as exc:
return ApiResponse(ok=False, message=exc.message) return ApiResponse(ok=False, message=exc.message)

View File

@@ -1,4 +1,4 @@
"""管理员 desktop_configs 键值 CRUD""" """OEM 桌面配置 CRUD仅当前 OEM 数据)"""
from fastapi import APIRouter, Query 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.schemas.common import ApiResponse, PaginatedData
from app.services import oem_desktop_config as config_service 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]]) @router.get("", response_model=ApiResponse[PaginatedData[DesktopConfigOut]])
async def list_configs( async def list_configs(
_oem: OemUser, oem: OemUser,
db: DbSession, db: DbSession,
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
page_size: int = Query( page_size: int = Query(
@@ -28,6 +28,7 @@ async def list_configs(
) -> ApiResponse[PaginatedData[DesktopConfigOut]]: ) -> ApiResponse[PaginatedData[DesktopConfigOut]]:
rows, total = await config_service.list_configs( rows, total = await config_service.list_configs(
db, db,
oem_user=oem,
page=page, page=page,
page_size=page_size, page_size=page_size,
keyword=keyword, keyword=keyword,
@@ -47,12 +48,12 @@ async def list_configs(
@router.post("", response_model=ApiResponse[DesktopConfigOut]) @router.post("", response_model=ApiResponse[DesktopConfigOut])
async def create_config( async def create_config(
body: DesktopConfigCreate, body: DesktopConfigCreate,
_oem: OemUser, oem: OemUser,
db: DbSession, db: DbSession,
) -> ApiResponse[DesktopConfigOut]: ) -> ApiResponse[DesktopConfigOut]:
try: try:
row = await config_service.create_config(db, body) row = await config_service.create_config(db, body, oem_user=oem)
except config_service.AdminDesktopConfigError as exc: except config_service.OemDesktopConfigError as exc:
return ApiResponse(ok=False, message=exc.message) return ApiResponse(ok=False, message=exc.message)
return ApiResponse( return ApiResponse(
@@ -66,12 +67,12 @@ async def create_config(
async def update_config( async def update_config(
config_id: int, config_id: int,
body: DesktopConfigUpdate, body: DesktopConfigUpdate,
_oem: OemUser, oem: OemUser,
db: DbSession, db: DbSession,
) -> ApiResponse[DesktopConfigOut]: ) -> ApiResponse[DesktopConfigOut]:
try: try:
row = await config_service.update_config(db, config_id, body) row = await config_service.update_config(db, config_id, body, oem_user=oem)
except config_service.AdminDesktopConfigError as exc: except config_service.OemDesktopConfigError as exc:
return ApiResponse(ok=False, message=exc.message) return ApiResponse(ok=False, message=exc.message)
return ApiResponse( return ApiResponse(
@@ -84,12 +85,12 @@ async def update_config(
@router.delete("/{config_id}", response_model=ApiResponse[None]) @router.delete("/{config_id}", response_model=ApiResponse[None])
async def delete_config( async def delete_config(
config_id: int, config_id: int,
_oem: OemUser, oem: OemUser,
db: DbSession, db: DbSession,
) -> ApiResponse[None]: ) -> ApiResponse[None]:
try: try:
await config_service.delete_config(db, config_id) await config_service.delete_config(db, config_id, oem_user=oem)
except config_service.AdminDesktopConfigError as exc: except config_service.OemDesktopConfigError as exc:
return ApiResponse(ok=False, message=exc.message) return ApiResponse(ok=False, message=exc.message)
return ApiResponse(ok=True, message="配置已删除") return ApiResponse(ok=True, message="配置已删除")

View File

@@ -5,7 +5,7 @@ from fastapi import APIRouter
from app.dependencies import OemUser, DbSession from app.dependencies import OemUser, DbSession
from app.schemas.admin_oem_branding import OemBrandingOut, OemBrandingUpdate from app.schemas.admin_oem_branding import OemBrandingOut, OemBrandingUpdate
from app.schemas.common import ApiResponse 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品牌"]) router = APIRouter(prefix="/oem/oem-branding", tags=["管理-OEM品牌"])
@@ -17,7 +17,7 @@ async def get_oem_branding(
) -> ApiResponse[OemBrandingOut]: ) -> ApiResponse[OemBrandingOut]:
try: try:
oem = await branding_service.get_default_oem_branding( 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: except branding_service.AdminOemBrandingError as exc:
return ApiResponse(ok=False, message=exc.message) return ApiResponse(ok=False, message=exc.message)
@@ -37,7 +37,7 @@ async def update_oem_branding(
) -> ApiResponse[OemBrandingOut]: ) -> ApiResponse[OemBrandingOut]:
try: try:
oem = await branding_service.update_default_oem_branding( 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: except branding_service.AdminOemBrandingError as exc:
return ApiResponse(ok=False, message=exc.message) return ApiResponse(ok=False, message=exc.message)

View File

@@ -18,18 +18,18 @@ def _map_image_error(exc: BrandingImageError) -> AdminOemBrandingError:
return AdminOemBrandingError(exc.message) return AdminOemBrandingError(exc.message)
async def get_or_create_default_oem(db: AsyncSession, *, owner_user_id: int) -> Oem: async def get_or_create_default_oem(db: AsyncSession, *, oem_id: int,user_id:int) -> Oem:
oem = await db.get(Oem, DEFAULT_OEM_ID) oem = await db.get(Oem, oem_id)
if oem is not None: if oem is not None:
return oem return oem
owner = await db.get(User, owner_user_id) owner = await db.get(User, user_id)
if owner is None: if owner is None:
raise AdminOemBrandingError("关联用户不存在") raise AdminOemBrandingError("关联用户不存在")
oem = Oem( oem = Oem(
id=DEFAULT_OEM_ID, id=DEFAULT_OEM_ID,
user_id=owner_user_id, user_id=user_id,
software_name="", software_name="",
) )
db.add(oem) db.add(oem)
@@ -38,17 +38,18 @@ async def get_or_create_default_oem(db: AsyncSession, *, owner_user_id: int) ->
return oem return oem
async def get_default_oem_branding(db: AsyncSession, *, owner_user_id: int) -> Oem: async def get_default_oem_branding(db: AsyncSession, *, oem_id: int,user_id:int) -> Oem:
return await get_or_create_default_oem(db, owner_user_id=owner_user_id) return await get_or_create_default_oem(db, oem_id=oem_id,user_id=user_id)
async def update_default_oem_branding( async def update_default_oem_branding(
db: AsyncSession, db: AsyncSession,
body: OemBrandingUpdate, body: OemBrandingUpdate,
*, *,
owner_user_id: int, oem_id: int,
user_id: int,
) -> Oem: ) -> 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: try:
logo_path = resolve_image_field( logo_path = resolve_image_field(

View File

@@ -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 sqlalchemy.ext.asyncio import AsyncSession
from app.models.desktopConfig import DesktopConfig 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)) result = await db.execute(select(DesktopConfig).where(DesktopConfig.oem_id == oem_id))
rows = result.scalars().all() rows = result.scalars().all()
return {row.name: row.value for row in rows} 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)

View File

@@ -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