11
This commit is contained in:
@@ -18,7 +18,12 @@ def _map_image_error(exc: BrandingImageError) -> AdminOemBrandingError:
|
||||
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)
|
||||
if oem is not None:
|
||||
return oem
|
||||
@@ -38,18 +43,23 @@ async def get_or_create_default_oem(db: AsyncSession, *, oem_id: int,user_id:int
|
||||
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, oem_id=oem_id,user_id=user_id)
|
||||
async def get_default_oem_branding(
|
||||
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(
|
||||
db: AsyncSession,
|
||||
body: OemBrandingUpdate,
|
||||
*,
|
||||
oem_id: int,
|
||||
oem_id: int = DEFAULT_OEM_ID,
|
||||
user_id: int,
|
||||
) -> 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:
|
||||
logo_path = resolve_image_field(
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import get_settings
|
||||
from app.core.heartbeat_verify import compute_server_verify_code
|
||||
from app.core.security import (
|
||||
create_access_token,
|
||||
hash_password,
|
||||
@@ -9,7 +12,7 @@ from app.core.security import (
|
||||
verify_password,
|
||||
)
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import TokenResponse, UserPublic
|
||||
from app.schemas.auth import HeartbeatResponse, TokenResponse, UserPublic
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
@@ -20,11 +23,44 @@ class AuthError(Exception):
|
||||
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(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
username: str,
|
||||
password: str,
|
||||
device_serial: str | None = None,
|
||||
) -> TokenResponse:
|
||||
existing = await db.scalar(select(User).where(User.username == username))
|
||||
if existing is not None:
|
||||
@@ -35,6 +71,7 @@ async def register_user(
|
||||
password_hash=hash_password(password),
|
||||
role_id=0,
|
||||
phone=None,
|
||||
device_serial=_normalize_device_serial(device_serial),
|
||||
)
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
@@ -52,11 +89,17 @@ async def login_user(
|
||||
*,
|
||||
username: str,
|
||||
password: str,
|
||||
device_serial: str | None = None,
|
||||
) -> TokenResponse:
|
||||
user = await db.scalar(select(User).where(User.username == username))
|
||||
if user is None or not verify_password(password, user.password_hash):
|
||||
raise AuthError("用户名或密码错误")
|
||||
|
||||
incoming = _normalize_device_serial(device_serial)
|
||||
await _sync_device_serial(
|
||||
db, user, incoming, mismatch_message="设备不匹配,无法登录"
|
||||
)
|
||||
|
||||
token = create_access_token(str(user.id))
|
||||
return TokenResponse(
|
||||
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:
|
||||
ttl = settings.access_token_expire_minutes * 60
|
||||
await redis_client.setex(session_redis_key(token), ttl, str(user_id))
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.oem_resolve import resolve_oem_id
|
||||
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
|
||||
|
||||
@@ -39,31 +39,6 @@ class OemDesktopConfigError(Exception):
|
||||
# 路由层沿用 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
|
||||
|
||||
@@ -1,77 +1,62 @@
|
||||
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
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
|
||||
from app.core.branding_images import BrandingImageError, resolve_image_field
|
||||
|
||||
from app.core.oem_resolve import get_or_create_oem_for_user, resolve_oem_id_for_user_id
|
||||
|
||||
from app.models.oem import Oem
|
||||
|
||||
from app.schemas.admin_oem_branding import OemBrandingUpdate
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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:
|
||||
|
||||
try:
|
||||
|
||||
return await get_or_create_oem_for_user(db, user_id)
|
||||
|
||||
except ValueError as exc:
|
||||
|
||||
raise AdminOemBrandingError(str(exc)) from exc
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async def get_default_oem_branding(db: AsyncSession, *, user_id: int) -> Oem:
|
||||
|
||||
return await get_or_create_default_oem(db, user_id=user_id)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async def update_default_oem_branding(
|
||||
|
||||
db: AsyncSession,
|
||||
|
||||
Reference in New Issue
Block a user