11
This commit is contained in:
@@ -4,11 +4,13 @@ import string
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.roles import ROLE_AGENT, ROLE_OEM
|
||||
from app.models.card_key import CardKey
|
||||
from app.models.user import User
|
||||
from app.schemas.admin_card_key import CardKeyCreate, CardKeyUpdate
|
||||
|
||||
DEFAULT_PAGE_SIZE = 20
|
||||
MAX_PAGE_SIZE = 100
|
||||
MAX_PAGE_SIZE = 200
|
||||
|
||||
_SERIAL_ALPHABET = string.ascii_uppercase + string.digits
|
||||
_SERIAL_LENGTH = 16
|
||||
@@ -43,22 +45,65 @@ async def _unique_serial(db: AsyncSession, preferred: str | None = None) -> str:
|
||||
raise AdminCardKeyError("生成序列号失败,请重试")
|
||||
|
||||
|
||||
async def _validate_assign_ids(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
oem_id: int | None,
|
||||
agent_id: int | None,
|
||||
) -> None:
|
||||
if oem_id is not None and agent_id is not None:
|
||||
raise AdminCardKeyError("OEM 与代理只能指定其一")
|
||||
if oem_id is not None:
|
||||
oem = await db.get(User, oem_id)
|
||||
if oem is None or oem.role_id != ROLE_OEM:
|
||||
raise AdminCardKeyError("所选 OEM 不存在")
|
||||
if agent_id is not None:
|
||||
agent = await db.get(User, agent_id)
|
||||
if agent is None or agent.role_id != ROLE_AGENT:
|
||||
raise AdminCardKeyError("所选代理不存在")
|
||||
|
||||
|
||||
def _list_card_keys_filters(
|
||||
*,
|
||||
status: str | None = None,
|
||||
username: str | None = None,
|
||||
oem_id: int | None = None,
|
||||
agent_id: int | None = None,
|
||||
) -> list:
|
||||
filters = []
|
||||
if status == "unused":
|
||||
filters.append(CardKey.activated_at.is_(None))
|
||||
elif status == "used":
|
||||
filters.append(CardKey.activated_at.is_not(None))
|
||||
if username and username.strip():
|
||||
filters.append(CardKey.username.ilike(f"%{username.strip()}%"))
|
||||
if oem_id is not None:
|
||||
filters.append(CardKey.oem_id == oem_id)
|
||||
if agent_id is not None:
|
||||
filters.append(CardKey.agent_id == agent_id)
|
||||
return filters
|
||||
|
||||
|
||||
async def list_card_keys(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = DEFAULT_PAGE_SIZE,
|
||||
status: str | None = None,
|
||||
username: str | None = None,
|
||||
oem_id: int | None = None,
|
||||
agent_id: int | None = None,
|
||||
) -> tuple[list[CardKey], int]:
|
||||
page_size = min(max(page_size, 1), MAX_PAGE_SIZE)
|
||||
page = max(page, 1)
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
filters = []
|
||||
if status == "unused":
|
||||
filters.append(CardKey.activated_at.is_(None))
|
||||
elif status == "used":
|
||||
filters.append(CardKey.activated_at.is_not(None))
|
||||
filters = _list_card_keys_filters(
|
||||
status=status,
|
||||
username=username,
|
||||
oem_id=oem_id,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
|
||||
count_stmt = select(func.count()).select_from(CardKey)
|
||||
list_stmt = select(CardKey)
|
||||
@@ -74,6 +119,7 @@ async def list_card_keys(
|
||||
|
||||
|
||||
async def create_card_keys(db: AsyncSession, body: CardKeyCreate) -> list[CardKey]:
|
||||
await _validate_assign_ids(db, oem_id=body.oem_id, agent_id=body.agent_id)
|
||||
created: list[CardKey] = []
|
||||
|
||||
for index in range(body.count):
|
||||
@@ -85,6 +131,8 @@ async def create_card_keys(db: AsyncSession, body: CardKeyCreate) -> list[CardKe
|
||||
serial_number=serial,
|
||||
duration_days=body.duration_days,
|
||||
remark=body.remark,
|
||||
oem_id=body.oem_id,
|
||||
agent_id=body.agent_id,
|
||||
)
|
||||
db.add(card)
|
||||
created.append(card)
|
||||
|
||||
@@ -5,7 +5,7 @@ from app.models.desktopConfig import DesktopConfig
|
||||
from app.schemas.admin_desktop_config import DesktopConfigCreate, DesktopConfigUpdate
|
||||
|
||||
DEFAULT_PAGE_SIZE = 20
|
||||
MAX_PAGE_SIZE = 100
|
||||
MAX_PAGE_SIZE = 200
|
||||
|
||||
|
||||
class AdminDesktopConfigError(Exception):
|
||||
|
||||
75
app/services/admin_oem_branding.py
Normal file
75
app/services/admin_oem_branding.py
Normal file
@@ -0,0 +1,75 @@
|
||||
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, *, owner_user_id: int) -> Oem:
|
||||
oem = await db.get(Oem, DEFAULT_OEM_ID)
|
||||
if oem is not None:
|
||||
return oem
|
||||
|
||||
owner = await db.get(User, owner_user_id)
|
||||
if owner is None:
|
||||
raise AdminOemBrandingError("关联用户不存在")
|
||||
|
||||
oem = Oem(
|
||||
id=DEFAULT_OEM_ID,
|
||||
user_id=owner_user_id,
|
||||
software_name="",
|
||||
)
|
||||
db.add(oem)
|
||||
await db.flush()
|
||||
await db.refresh(oem)
|
||||
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 update_default_oem_branding(
|
||||
db: AsyncSession,
|
||||
body: OemBrandingUpdate,
|
||||
*,
|
||||
owner_user_id: int,
|
||||
) -> Oem:
|
||||
oem = await get_or_create_default_oem(db, owner_user_id=owner_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
|
||||
@@ -1,13 +1,13 @@
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.roles import ROLE_ADMIN
|
||||
from app.core.roles import ROLE_ADMIN, ROLE_AGENT, ROLE_OEM
|
||||
from app.core.security import hash_password
|
||||
from app.models.user import User
|
||||
from app.schemas.admin_user import AdminUserCreate, AdminUserUpdate
|
||||
|
||||
DEFAULT_PAGE_SIZE = 20
|
||||
MAX_PAGE_SIZE = 100
|
||||
MAX_PAGE_SIZE = 200
|
||||
|
||||
|
||||
class AdminUserError(Exception):
|
||||
@@ -16,23 +16,72 @@ class AdminUserError(Exception):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
def _list_users_filters(
|
||||
*,
|
||||
username: str | None = None,
|
||||
oem_id: int | None = None,
|
||||
agent_id: int | None = None,
|
||||
role_id: int | None = None,
|
||||
) -> list:
|
||||
conditions = []
|
||||
if username and username.strip():
|
||||
conditions.append(User.username.ilike(f"%{username.strip()}%"))
|
||||
if oem_id is not None:
|
||||
conditions.append(User.oem_id == oem_id)
|
||||
if agent_id is not None:
|
||||
conditions.append(User.agent_id == agent_id)
|
||||
if role_id is not None:
|
||||
conditions.append(User.role_id == role_id)
|
||||
return conditions
|
||||
|
||||
|
||||
async def list_users(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = DEFAULT_PAGE_SIZE,
|
||||
username: str | None = None,
|
||||
oem_id: int | None = None,
|
||||
agent_id: int | None = None,
|
||||
role_id: int | None = None,
|
||||
) -> tuple[list[User], int]:
|
||||
page_size = min(max(page_size, 1), MAX_PAGE_SIZE)
|
||||
page = max(page, 1)
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
total = await db.scalar(select(func.count()).select_from(User)) or 0
|
||||
result = await db.scalars(
|
||||
select(User).order_by(User.id.desc()).offset(offset).limit(page_size)
|
||||
conditions = _list_users_filters(
|
||||
username=username,
|
||||
oem_id=oem_id,
|
||||
agent_id=agent_id,
|
||||
role_id=role_id,
|
||||
)
|
||||
|
||||
count_stmt = select(func.count()).select_from(User)
|
||||
list_stmt = select(User).order_by(User.id.desc())
|
||||
if conditions:
|
||||
count_stmt = count_stmt.where(*conditions)
|
||||
list_stmt = list_stmt.where(*conditions)
|
||||
|
||||
total = await db.scalar(count_stmt) or 0
|
||||
result = await db.scalars(list_stmt.offset(offset).limit(page_size))
|
||||
return list(result.all()), total
|
||||
|
||||
|
||||
async def _validate_owner_ids(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
oem_id: int | None,
|
||||
agent_id: int | None,
|
||||
) -> None:
|
||||
if oem_id is not None:
|
||||
oem = await db.get(User, oem_id)
|
||||
if oem is None or oem.role_id != ROLE_OEM:
|
||||
raise AdminUserError("所选 OEM 不存在")
|
||||
if agent_id is not None:
|
||||
agent = await db.get(User, agent_id)
|
||||
if agent is None or agent.role_id != ROLE_AGENT:
|
||||
raise AdminUserError("所选代理不存在")
|
||||
|
||||
|
||||
async def create_user(db: AsyncSession, body: AdminUserCreate) -> User:
|
||||
existing = await db.scalar(select(User).where(User.username == body.username))
|
||||
if existing is not None:
|
||||
@@ -43,11 +92,15 @@ async def create_user(db: AsyncSession, body: AdminUserCreate) -> User:
|
||||
if phone_taken is not None:
|
||||
raise AdminUserError("手机号已被使用")
|
||||
|
||||
await _validate_owner_ids(db, oem_id=body.oem_id, agent_id=body.agent_id)
|
||||
|
||||
user = User(
|
||||
username=body.username,
|
||||
password_hash=hash_password(body.password),
|
||||
phone=body.phone,
|
||||
role_id=body.role_id,
|
||||
oem_id=body.oem_id,
|
||||
agent_id=body.agent_id,
|
||||
vip_end_time=body.vip_end_time,
|
||||
)
|
||||
db.add(user)
|
||||
@@ -85,6 +138,17 @@ async def update_user(
|
||||
raise AdminUserError("不能修改自己的管理员角色")
|
||||
user.role_id = body.role_id
|
||||
|
||||
if body.oem_id is not None or body.agent_id is not None:
|
||||
await _validate_owner_ids(
|
||||
db,
|
||||
oem_id=body.oem_id if body.oem_id is not None else user.oem_id,
|
||||
agent_id=body.agent_id if body.agent_id is not None else user.agent_id,
|
||||
)
|
||||
if body.oem_id is not None:
|
||||
user.oem_id = body.oem_id
|
||||
if body.agent_id is not None:
|
||||
user.agent_id = body.agent_id
|
||||
|
||||
if body.password:
|
||||
user.password_hash = hash_password(body.password)
|
||||
|
||||
|
||||
31
app/services/oem_public.py
Normal file
31
app/services/oem_public.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models.oem import Oem
|
||||
from app.schemas.oem_public import OemPublicBrandingOut
|
||||
|
||||
|
||||
def _logo_url(logo_path: str | None, base_url: str) -> str | None:
|
||||
if not logo_path or not logo_path.strip():
|
||||
return None
|
||||
path = logo_path.strip().replace("\\", "/").lstrip("/")
|
||||
if path.startswith("http://") or path.startswith("https://"):
|
||||
return path
|
||||
if not path.startswith("images/"):
|
||||
path = f"images/{path.removeprefix('images/')}"
|
||||
return f"{base_url.rstrip('/')}/{path}"
|
||||
|
||||
|
||||
async def get_oem_public_branding(db: AsyncSession, oem_id: int) -> OemPublicBrandingOut:
|
||||
settings = get_settings()
|
||||
base_url = settings.api_public_base.rstrip("/")
|
||||
oem = await db.get(Oem, oem_id)
|
||||
if oem is None:
|
||||
return OemPublicBrandingOut(oem_id=oem_id, software_name="", logo_url=None, wechat=None)
|
||||
|
||||
return OemPublicBrandingOut(
|
||||
oem_id=oem.id,
|
||||
software_name=oem.software_name or "",
|
||||
logo_url=_logo_url(oem.logo_path, base_url),
|
||||
wechat=oem.wechat,
|
||||
)
|
||||
Reference in New Issue
Block a user