11
This commit is contained in:
28
alembic/versions/011_add_user_device_serial.py
Normal file
28
alembic/versions/011_add_user_device_serial.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""add users.device_serial
|
||||||
|
|
||||||
|
Revision ID: 011
|
||||||
|
Revises: 010
|
||||||
|
Create Date: 2026-05-22
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "011"
|
||||||
|
down_revision: Union[str, None] = "010"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"users",
|
||||||
|
sa.Column("device_serial", sa.String(255), nullable=False, server_default=""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("users", "device_serial")
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
from fastapi import APIRouter, Header
|
from fastapi import APIRouter, Header
|
||||||
|
|
||||||
from app.dependencies import CurrentUser, DbSession, RedisClient
|
from app.dependencies import CurrentUser, DbSession, RedisClient
|
||||||
from app.schemas.auth import LoginRequest, RegisterRequest, TokenResponse, UserPublic
|
from app.schemas.auth import (
|
||||||
|
HeartbeatRequest,
|
||||||
|
HeartbeatResponse,
|
||||||
|
LoginRequest,
|
||||||
|
RegisterRequest,
|
||||||
|
TokenResponse,
|
||||||
|
UserPublic,
|
||||||
|
)
|
||||||
from app.schemas.common import ApiResponse
|
from app.schemas.common import ApiResponse
|
||||||
from app.services import auth as auth_service
|
from app.services import auth as auth_service
|
||||||
|
|
||||||
@@ -13,12 +20,14 @@ async def register(
|
|||||||
body: RegisterRequest,
|
body: RegisterRequest,
|
||||||
db: DbSession,
|
db: DbSession,
|
||||||
redis: RedisClient,
|
redis: RedisClient,
|
||||||
|
device_serial: str | None = Header(default=None, convert_underscores=False),
|
||||||
) -> ApiResponse[TokenResponse]:
|
) -> ApiResponse[TokenResponse]:
|
||||||
try:
|
try:
|
||||||
result = await auth_service.register_user(
|
result = await auth_service.register_user(
|
||||||
db,
|
db,
|
||||||
username=body.username,
|
username=body.username,
|
||||||
password=body.password,
|
password=body.password,
|
||||||
|
device_serial=device_serial,
|
||||||
)
|
)
|
||||||
except auth_service.AuthError as exc:
|
except auth_service.AuthError as exc:
|
||||||
return ApiResponse(ok=False, message=exc.message)
|
return ApiResponse(ok=False, message=exc.message)
|
||||||
@@ -32,12 +41,14 @@ async def login(
|
|||||||
body: LoginRequest,
|
body: LoginRequest,
|
||||||
db: DbSession,
|
db: DbSession,
|
||||||
redis: RedisClient,
|
redis: RedisClient,
|
||||||
|
device_serial: str | None = Header(default=None, convert_underscores=False),
|
||||||
) -> ApiResponse[TokenResponse]:
|
) -> ApiResponse[TokenResponse]:
|
||||||
try:
|
try:
|
||||||
result = await auth_service.login_user(
|
result = await auth_service.login_user(
|
||||||
db,
|
db,
|
||||||
username=body.username,
|
username=body.username,
|
||||||
password=body.password,
|
password=body.password,
|
||||||
|
device_serial=device_serial,
|
||||||
)
|
)
|
||||||
except auth_service.AuthError as exc:
|
except auth_service.AuthError as exc:
|
||||||
return ApiResponse(ok=False, message=exc.message)
|
return ApiResponse(ok=False, message=exc.message)
|
||||||
@@ -46,6 +57,25 @@ async def login(
|
|||||||
return ApiResponse(ok=True, message="登录成功", data=result)
|
return ApiResponse(ok=True, message="登录成功", data=result)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/heartbeat", response_model=ApiResponse[HeartbeatResponse])
|
||||||
|
async def heartbeat(
|
||||||
|
body: HeartbeatRequest,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: DbSession,
|
||||||
|
device_serial: str | None = Header(default=None, convert_underscores=False),
|
||||||
|
) -> ApiResponse[HeartbeatResponse]:
|
||||||
|
try:
|
||||||
|
data = await auth_service.process_heartbeat(
|
||||||
|
db,
|
||||||
|
current_user,
|
||||||
|
device_serial,
|
||||||
|
body.verify_code,
|
||||||
|
)
|
||||||
|
except auth_service.AuthError as exc:
|
||||||
|
return ApiResponse(ok=False, message=exc.message)
|
||||||
|
return ApiResponse(ok=True, message="", data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/logout", response_model=ApiResponse[None])
|
@router.post("/logout", response_model=ApiResponse[None])
|
||||||
async def logout(
|
async def logout(
|
||||||
redis: RedisClient,
|
redis: RedisClient,
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ async def get_oem_branding(
|
|||||||
db: DbSession,
|
db: DbSession,
|
||||||
) -> ApiResponse[OemBrandingOut]:
|
) -> ApiResponse[OemBrandingOut]:
|
||||||
try:
|
try:
|
||||||
oem = await branding_service.get_default_oem_branding(
|
oem = await branding_service.get_default_oem_branding(db, 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)
|
||||||
|
|
||||||
@@ -36,9 +34,7 @@ async def update_oem_branding(
|
|||||||
db: DbSession,
|
db: DbSession,
|
||||||
) -> ApiResponse[OemBrandingOut]:
|
) -> ApiResponse[OemBrandingOut]:
|
||||||
try:
|
try:
|
||||||
oem = await branding_service.update_default_oem_branding(
|
oem = await branding_service.update_default_oem_branding(db, body, 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)
|
||||||
|
|
||||||
|
|||||||
20
app/core/heartbeat_verify.py
Normal file
20
app/core/heartbeat_verify.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""心跳响应防伪:MD5(时间桶 + verify_code)。"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
def heartbeat_time_bucket(dt: datetime | None = None) -> str:
|
||||||
|
"""UTC 时间桶,格式如 ``2026-05-20 15``(精确到小时)。"""
|
||||||
|
if dt is None:
|
||||||
|
dt = datetime.now(timezone.utc)
|
||||||
|
elif dt.tzinfo is None:
|
||||||
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
|
else:
|
||||||
|
dt = dt.astimezone(timezone.utc)
|
||||||
|
return dt.strftime("%Y-%m-%d %H")
|
||||||
|
|
||||||
|
|
||||||
|
def compute_server_verify_code(verify_code: str, dt: datetime | None = None) -> str:
|
||||||
|
raw = f"{heartbeat_time_bucket(dt)}{verify_code}"
|
||||||
|
return hashlib.md5(raw.encode("utf-8")).hexdigest()
|
||||||
57
app/core/oem_resolve.py
Normal file
57
app/core/oem_resolve.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"""OEM 用户与 oem 表主键解析(全局缓存,多模块共用)。"""
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.oem import Oem
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
# user_id -> oem 表主键(desktop_configs.oem_id 等同此值)
|
||||||
|
_oem_id_by_user_id: dict[int, int] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_oem_id_cache(user_id: int | None = None) -> None:
|
||||||
|
"""清除 resolve 缓存;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_for_user_id(db: AsyncSession, user_id: int) -> int:
|
||||||
|
"""
|
||||||
|
解析 OEM 用户在业务表中的 oem_id(oem 表主键)。
|
||||||
|
若尚无 oem 记录,回退为 user_id(与 desktop_configs 历史逻辑一致)。
|
||||||
|
"""
|
||||||
|
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 resolve_oem_id(db: AsyncSession, oem_user: User) -> int:
|
||||||
|
"""根据 User 模型解析 oem_id。"""
|
||||||
|
return await resolve_oem_id_for_user_id(db, oem_user.id)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_or_create_oem_for_user(db: AsyncSession, user_id: int) -> Oem:
|
||||||
|
"""按 user_id 获取 oem 行;不存在则创建并刷新缓存。"""
|
||||||
|
existing = await db.scalar(select(Oem).where(Oem.user_id == user_id))
|
||||||
|
if existing is not None:
|
||||||
|
_oem_id_by_user_id[user_id] = existing.id
|
||||||
|
return existing
|
||||||
|
|
||||||
|
owner = await db.get(User, user_id)
|
||||||
|
if owner is None:
|
||||||
|
raise ValueError("关联用户不存在")
|
||||||
|
|
||||||
|
oem = Oem(user_id=user_id, software_name="")
|
||||||
|
db.add(oem)
|
||||||
|
await db.flush()
|
||||||
|
await db.refresh(oem)
|
||||||
|
_oem_id_by_user_id[user_id] = oem.id
|
||||||
|
return oem
|
||||||
@@ -25,3 +25,4 @@ class User(Base):
|
|||||||
)
|
)
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
device_serial: Mapped[str] = mapped_column(String(255), default="", server_default="")
|
||||||
@@ -47,3 +47,15 @@ class TokenResponse(BaseModel):
|
|||||||
access_token: str
|
access_token: str
|
||||||
token_type: str = "bearer"
|
token_type: str = "bearer"
|
||||||
user: UserPublic
|
user: UserPublic
|
||||||
|
|
||||||
|
|
||||||
|
class HeartbeatRequest(BaseModel):
|
||||||
|
verify_code: str = Field(min_length=1, max_length=128)
|
||||||
|
|
||||||
|
|
||||||
|
class HeartbeatResponse(BaseModel):
|
||||||
|
"""心跳成功时的状态快照。"""
|
||||||
|
|
||||||
|
vip_end_time: datetime | None = None
|
||||||
|
vip_active: bool
|
||||||
|
server_verify_code: str
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ def _map_image_error(exc: BrandingImageError) -> AdminOemBrandingError:
|
|||||||
return AdminOemBrandingError(exc.message)
|
return AdminOemBrandingError(exc.message)
|
||||||
|
|
||||||
|
|
||||||
async def get_or_create_default_oem(db: AsyncSession, *, oem_id: int,user_id:int) -> Oem:
|
async def get_or_create_default_oem(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
oem_id: int = DEFAULT_OEM_ID,
|
||||||
|
user_id: int,
|
||||||
|
) -> Oem:
|
||||||
oem = await db.get(Oem, oem_id)
|
oem = await db.get(Oem, oem_id)
|
||||||
if oem is not None:
|
if oem is not None:
|
||||||
return oem
|
return oem
|
||||||
@@ -38,18 +43,23 @@ async def get_or_create_default_oem(db: AsyncSession, *, oem_id: int,user_id:int
|
|||||||
return oem
|
return oem
|
||||||
|
|
||||||
|
|
||||||
async def get_default_oem_branding(db: AsyncSession, *, oem_id: int,user_id:int) -> Oem:
|
async def get_default_oem_branding(
|
||||||
return await get_or_create_default_oem(db, oem_id=oem_id,user_id=user_id)
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
oem_id: int = DEFAULT_OEM_ID,
|
||||||
|
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(
|
async def update_default_oem_branding(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
body: OemBrandingUpdate,
|
body: OemBrandingUpdate,
|
||||||
*,
|
*,
|
||||||
oem_id: int,
|
oem_id: int = DEFAULT_OEM_ID,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
) -> Oem:
|
) -> Oem:
|
||||||
oem = await get_or_create_default_oem(db, oem_id=oem_id,user_id=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(
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
|
from app.core.heartbeat_verify import compute_server_verify_code
|
||||||
from app.core.security import (
|
from app.core.security import (
|
||||||
create_access_token,
|
create_access_token,
|
||||||
hash_password,
|
hash_password,
|
||||||
@@ -9,7 +12,7 @@ from app.core.security import (
|
|||||||
verify_password,
|
verify_password,
|
||||||
)
|
)
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.auth import TokenResponse, UserPublic
|
from app.schemas.auth import HeartbeatResponse, TokenResponse, UserPublic
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
|
||||||
@@ -20,11 +23,44 @@ class AuthError(Exception):
|
|||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_device_serial(value: str | None) -> str:
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
return value.strip()[:255]
|
||||||
|
|
||||||
|
|
||||||
|
def _vip_is_active(vip_end_time: datetime | None) -> bool:
|
||||||
|
if vip_end_time is None:
|
||||||
|
return False
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
end = vip_end_time
|
||||||
|
if end.tzinfo is None:
|
||||||
|
end = end.replace(tzinfo=timezone.utc)
|
||||||
|
return end > now
|
||||||
|
|
||||||
|
|
||||||
|
async def _sync_device_serial(
|
||||||
|
db: AsyncSession,
|
||||||
|
user: User,
|
||||||
|
incoming: str,
|
||||||
|
*,
|
||||||
|
mismatch_message: str,
|
||||||
|
) -> None:
|
||||||
|
stored = user.device_serial or ""
|
||||||
|
if not stored:
|
||||||
|
if incoming:
|
||||||
|
user.device_serial = incoming
|
||||||
|
await db.flush()
|
||||||
|
elif incoming != stored:
|
||||||
|
raise AuthError(mismatch_message)
|
||||||
|
|
||||||
|
|
||||||
async def register_user(
|
async def register_user(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
*,
|
*,
|
||||||
username: str,
|
username: str,
|
||||||
password: str,
|
password: str,
|
||||||
|
device_serial: str | None = None,
|
||||||
) -> TokenResponse:
|
) -> TokenResponse:
|
||||||
existing = await db.scalar(select(User).where(User.username == username))
|
existing = await db.scalar(select(User).where(User.username == username))
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
@@ -35,6 +71,7 @@ async def register_user(
|
|||||||
password_hash=hash_password(password),
|
password_hash=hash_password(password),
|
||||||
role_id=0,
|
role_id=0,
|
||||||
phone=None,
|
phone=None,
|
||||||
|
device_serial=_normalize_device_serial(device_serial),
|
||||||
)
|
)
|
||||||
db.add(user)
|
db.add(user)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
@@ -52,11 +89,17 @@ async def login_user(
|
|||||||
*,
|
*,
|
||||||
username: str,
|
username: str,
|
||||||
password: str,
|
password: str,
|
||||||
|
device_serial: str | None = None,
|
||||||
) -> TokenResponse:
|
) -> TokenResponse:
|
||||||
user = await db.scalar(select(User).where(User.username == username))
|
user = await db.scalar(select(User).where(User.username == username))
|
||||||
if user is None or not verify_password(password, user.password_hash):
|
if user is None or not verify_password(password, user.password_hash):
|
||||||
raise AuthError("用户名或密码错误")
|
raise AuthError("用户名或密码错误")
|
||||||
|
|
||||||
|
incoming = _normalize_device_serial(device_serial)
|
||||||
|
await _sync_device_serial(
|
||||||
|
db, user, incoming, mismatch_message="设备不匹配,无法登录"
|
||||||
|
)
|
||||||
|
|
||||||
token = create_access_token(str(user.id))
|
token = create_access_token(str(user.id))
|
||||||
return TokenResponse(
|
return TokenResponse(
|
||||||
access_token=token,
|
access_token=token,
|
||||||
@@ -64,6 +107,29 @@ async def login_user(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def process_heartbeat(
|
||||||
|
db: AsyncSession,
|
||||||
|
user: User,
|
||||||
|
device_serial: str | None,
|
||||||
|
verify_code: str,
|
||||||
|
) -> HeartbeatResponse:
|
||||||
|
incoming = _normalize_device_serial(device_serial)
|
||||||
|
await _sync_device_serial(db, user, incoming, mismatch_message="设备不匹配")
|
||||||
|
|
||||||
|
if not _vip_is_active(user.vip_end_time):
|
||||||
|
raise AuthError("会员已过期或未开通")
|
||||||
|
|
||||||
|
code = verify_code.strip()
|
||||||
|
if not code:
|
||||||
|
raise AuthError("verify_code 无效")
|
||||||
|
|
||||||
|
return HeartbeatResponse(
|
||||||
|
vip_end_time=user.vip_end_time,
|
||||||
|
vip_active=True,
|
||||||
|
server_verify_code=compute_server_verify_code(code),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def store_session(redis_client, token: str, user_id: int) -> None:
|
async def store_session(redis_client, token: str, user_id: int) -> None:
|
||||||
ttl = settings.access_token_expire_minutes * 60
|
ttl = settings.access_token_expire_minutes * 60
|
||||||
await redis_client.setex(session_redis_key(token), ttl, str(user_id))
|
await redis_client.setex(session_redis_key(token), ttl, str(user_id))
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.oem_resolve import resolve_oem_id
|
||||||
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.models.user import User
|
||||||
from app.schemas.admin_desktop_config import DesktopConfigCreate, DesktopConfigUpdate
|
from app.schemas.admin_desktop_config import DesktopConfigCreate, DesktopConfigUpdate
|
||||||
|
|
||||||
@@ -39,31 +39,6 @@ class OemDesktopConfigError(Exception):
|
|||||||
# 路由层沿用 AdminDesktopConfigError 名称
|
# 路由层沿用 AdminDesktopConfigError 名称
|
||||||
AdminDesktopConfigError = OemDesktopConfigError
|
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:
|
async def _count_by_oem(db: AsyncSession, oem_id: int) -> int:
|
||||||
stmt = select(func.count()).select_from(DesktopConfig).where(DesktopConfig.oem_id == oem_id)
|
stmt = select(func.count()).select_from(DesktopConfig).where(DesktopConfig.oem_id == oem_id)
|
||||||
return await db.scalar(stmt) or 0
|
return await db.scalar(stmt) or 0
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from app.core.branding_images import BrandingImageError, resolve_image_field
|
from app.core.branding_images import BrandingImageError, resolve_image_field
|
||||||
from app.models.user import User
|
|
||||||
|
|
||||||
from app.core.oem_resolve import get_or_create_oem_for_user, resolve_oem_id_for_user_id
|
from app.core.oem_resolve import get_or_create_oem_for_user, resolve_oem_id_for_user_id
|
||||||
DEFAULT_OEM_ID = 1
|
|
||||||
|
|
||||||
|
|
||||||
from app.models.oem import Oem
|
from app.models.oem import Oem
|
||||||
|
|
||||||
@@ -18,28 +16,14 @@ def _map_image_error(exc: BrandingImageError) -> AdminOemBrandingError:
|
|||||||
|
|
||||||
class AdminOemBrandingError(Exception):
|
class AdminOemBrandingError(Exception):
|
||||||
|
|
||||||
async def get_or_create_default_oem(db: AsyncSession, *, user_id:int) -> Oem:
|
def __init__(self, message: str) -> None:
|
||||||
oem_id = await db.scalar(select(Oem.id).where(Oem.user_id == user_id))
|
|
||||||
oem = await db.get(Oem, oem_id)
|
self.message = message
|
||||||
if oem is not None:
|
|
||||||
return oem
|
super().__init__(message)
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
|
|
||||||
def _map_image_error(exc: BrandingImageError) -> AdminOemBrandingError:
|
def _map_image_error(exc: BrandingImageError) -> AdminOemBrandingError:
|
||||||
@@ -47,9 +31,9 @@ async def update_default_oem_branding(
|
|||||||
return AdminOemBrandingError(exc.message)
|
return AdminOemBrandingError(exc.message)
|
||||||
|
|
||||||
|
|
||||||
oem_id: int,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def get_or_create_default_oem(db: AsyncSession, *, user_id: int) -> Oem:
|
async def get_or_create_default_oem(db: AsyncSession, *, user_id: int) -> Oem:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -75,3 +59,4 @@ async def update_default_oem_branding(
|
|||||||
async def update_default_oem_branding(
|
async def update_default_oem_branding(
|
||||||
|
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
|
|
||||||
BIN
images/oem_logo_edf78a4935d7.png
Normal file
BIN
images/oem_logo_edf78a4935d7.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
Reference in New Issue
Block a user