新增查看站点概况

This commit is contained in:
2026-01-08 14:41:10 +08:00
parent c17511d2e8
commit baf59b9903
6 changed files with 417 additions and 25 deletions

View File

@@ -90,4 +90,10 @@ from .model_base import (
MCPRequestBase,
MCPResponseBase,
ResponseBase,
# Admin Summary DTO
MetricsSummary,
LicenseInfo,
VersionInfo,
AdminSummaryData,
AdminSummaryResponse,
)

View File

@@ -1,16 +1,107 @@
import uuid
from datetime import datetime
from enum import StrEnum
from sqlmodel import Field
from .base import SQLModelBase
class ResponseBase(SQLModelBase):
"""通用响应模型"""
instance_id: uuid.UUID = Field(default_factory=uuid.uuid4)
"""实例ID用于标识请求的唯一性"""
# ==================== Admin Summary DTO ====================
class MetricsSummary(SQLModelBase):
"""站点统计摘要"""
dates: list[datetime]
"""日期列表"""
files: list[int]
"""每日新增文件数"""
users: list[int]
"""每日新增用户数"""
shares: list[int]
"""每日新增分享数"""
file_total: int
"""文件总数"""
user_total: int
"""用户总数"""
share_total: int
"""分享总数"""
entities_total: int
"""实体总数"""
generated_at: datetime
"""生成时间"""
class LicenseInfo(SQLModelBase):
"""许可证信息"""
expired_at: datetime
"""过期时间"""
signed_at: datetime
"""签发时间"""
root_domains: list[str]
"""根域名列表"""
domains: list[str]
"""域名列表"""
vol_domains: list[str]
"""卷域名列表"""
class VersionInfo(SQLModelBase):
"""版本信息"""
version: str
"""版本号"""
pro: bool
"""是否为专业版"""
commit: str
"""提交哈希"""
class AdminSummaryData(SQLModelBase):
"""管理员概况数据"""
metrics_summary: MetricsSummary
"""统计摘要"""
site_urls: list[str]
"""站点URL列表"""
license: LicenseInfo
"""许可证信息"""
version: VersionInfo
"""版本信息"""
class AdminSummaryResponse(ResponseBase):
"""管理员概况响应"""
data: AdminSummaryData | None = None
"""响应数据"""
class MCPMethod(StrEnum):
"""MCP 方法枚举"""

View File

@@ -84,3 +84,70 @@ class Share(SQLModelBase, TableBaseMixin):
"""是否为目录分享(向后兼容属性)"""
from .object import ObjectType
return self.object.type == ObjectType.FOLDER if self.object else False
# ==================== DTO 模型 ====================
class ShareCreateRequest(SQLModelBase):
"""创建分享请求 DTO"""
object_id: UUID
"""要分享的对象UUID"""
password: str | None = None
"""分享密码(可选)"""
expires: datetime | None = None
"""过期时间可选NULL为永不过期"""
remain_downloads: int | None = None
"""剩余下载次数可选NULL为不限制"""
preview_enabled: bool = True
"""是否允许预览"""
score: int = 0
"""兑换此分享所需的积分"""
class ShareResponse(SQLModelBase):
"""分享响应 DTO"""
id: int
"""分享ID"""
code: str
"""分享码"""
object_id: UUID
"""关联对象UUID"""
source_name: str | None
"""源名称"""
views: int
"""浏览次数"""
downloads: int
"""下载次数"""
remain_downloads: int | None
"""剩余下载次数"""
expires: datetime | None
"""过期时间"""
preview_enabled: bool
"""是否允许预览"""
score: int
"""积分"""
created_at: datetime
"""创建时间"""
is_expired: bool
"""是否已过期"""
has_password: bool
"""是否有密码"""

View File

@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timedelta
from typing import Annotated
from uuid import UUID
@@ -13,7 +13,10 @@ from middleware.dependencies import SessionDep
from models import (
Policy, PolicyOptions, PolicyType, User, ResponseBase,
Group, GroupOptions, Setting, Object, ObjectType, Share, Task,
AdminSummaryResponse, MetricsSummary, LicenseInfo, VersionInfo, AdminSummaryData,
)
from models.setting import SettingsType
from utils.conf import appmeta
from models.base import SQLModelBase
from models.group import (
GroupCreateRequest, GroupUpdateRequest, GroupDetailResponse, GroupListResponse,
@@ -25,7 +28,7 @@ from models.setting import SettingsUpdateRequest, SettingsGetResponse
from models.object import AdminFileResponse, AdminFileListResponse, FileBanRequest
from models.policy import GroupPolicyLink
from service.storage import DirectoryCreationError, LocalStorageService
from utils import Password
from utils import Password, http_exceptions
class PolicyTestPathRequest(SQLModelBase):
@@ -158,14 +161,108 @@ admin_vas_router = APIRouter(
description='Get site summary information',
dependencies=[Depends(admin_required)],
)
def router_admin_get_summary() -> ResponseBase:
async def router_admin_get_summary(session: SessionDep) -> AdminSummaryResponse:
"""
获取站点概况信息,包括用户数、分享数、文件数等。
获取站点概况信息,包括用户数、分享数、文件数等统计指标
响应数据结构:
- metrics_summary: 统计摘要(日期列表、每日增量、总计)
- site_urls: 站点URL列表
- license: 许可证信息(过期时间、签发时间、域名配置)
- version: 版本信息版本号、是否Pro、提交哈希
Returns:
ResponseBase: 包含站点概况信息的响应模型。
AdminSummaryResponse: 包含站点概况信息的响应模型。
"""
http_exceptions.raise_not_implemented()
# 统计最近 12 天的数据
days_count = 12
now = datetime.now()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
dates: list[datetime] = []
files: list[int] = []
users: list[int] = []
shares: list[int] = []
# 从 11 天前到今天
for i in range(days_count - 1, -1, -1):
day_start = today_start - timedelta(days=i)
day_end = day_start + timedelta(days=1)
dates.append(day_start)
# 统计每日新增
file_count = await Object.count(
session,
Object.type == ObjectType.FILE,
created_after_datetime=day_start,
created_before_datetime=day_end,
)
user_count = await User.count(
session,
created_after_datetime=day_start,
created_before_datetime=day_end,
)
share_count = await Share.count(
session,
created_after_datetime=day_start,
created_before_datetime=day_end,
)
files.append(file_count)
users.append(user_count)
shares.append(share_count)
# 统计总数
file_total = await Object.count(session, Object.type == ObjectType.FILE)
user_total = await User.count(session)
share_total = await Share.count(session)
entities_total = await Object.count(session)
metrics_summary = MetricsSummary(
dates=dates,
files=files,
users=users,
shares=shares,
file_total=file_total,
user_total=user_total,
share_total=share_total,
entities_total=entities_total,
generated_at=now,
)
# 获取站点 URL从设置读取
site_urls: list[str] = []
site_url_setting = await Setting.get(
session,
and_(Setting.type == SettingsType.BASIC, Setting.name == "siteURL"),
)
if site_url_setting and site_url_setting.value:
site_urls.append(site_url_setting.value)
# 许可证信息(从设置读取或使用默认值)
license_info = LicenseInfo(
expired_at=now + timedelta(days=365),
signed_at=now,
root_domains=[],
domains=[],
vol_domains=[],
)
# 版本信息
version_info = VersionInfo(
version=appmeta.BackendVersion,
pro=appmeta.IsPro,
commit="dev",
)
data = AdminSummaryData(
metrics_summary=metrics_summary,
site_urls=site_urls,
license=license_info,
version=version_info,
)
return AdminSummaryResponse(data=data)
@admin_router.get(
path='/news',

View File

@@ -128,6 +128,10 @@ async def _copy_object_recursive(
copied_count = 0
new_ids: list[UUID] = []
# 在 save() 之前保存需要的属性值,避免 commit 后对象过期导致懒加载失败
src_is_folder = src.is_folder
src_id = src.id
# 创建新的 Object 记录
new_obj = Object(
name=src.name,
@@ -152,8 +156,8 @@ async def _copy_object_recursive(
new_ids.append(new_obj.id)
# 如果是目录,递归复制子对象
if src.is_folder:
children = await Object.get_children(session, user_id, src.id)
if src_is_folder:
children = await Object.get_children(session, user_id, src_id)
for child in children:
child_count, child_ids = await _copy_object_recursive(
session, child, new_obj.id, user_id

View File

@@ -1,8 +1,19 @@
from fastapi import APIRouter, Depends
from typing import Annotated, Literal
from uuid import uuid4
from datetime import datetime
from fastapi import APIRouter, Depends, Query, HTTPException
from loguru import logger as l
from middleware.auth import auth_required
from middleware.dependencies import SessionDep
from models import ResponseBase
from models.user import User
from models.share import Share, ShareCreateRequest, ShareResponse
from models.object import Object
from models.mixin import ListResponse, TableViewRequest
from utils import http_exceptions
from utils.password.pwd import Password
share_router = APIRouter(
prefix='/share',
@@ -10,7 +21,7 @@ share_router = APIRouter(
)
@share_router.get(
path='/{info}/{id}',
path='/{id}',
summary='获取分享',
description='Get shared content by info type and ID.',
)
@@ -227,31 +238,147 @@ def router_share_search_public(keywords: str, type: str = 'all') -> ResponseBase
path='/',
summary='创建新分享',
description='Create a new share endpoint.',
dependencies=[Depends(auth_required)]
)
def router_share_create() -> ResponseBase:
async def router_share_create(
session: SessionDep,
user: Annotated[User, Depends(auth_required)],
request: ShareCreateRequest,
) -> ShareResponse:
"""
Create a new share endpoint.
Returns:
ResponseBase: A model containing the response data for the new share creation.
创建新分享
认证:需要 JWT token
流程:
1. 验证对象存在且属于当前用户
2. 生成随机分享码uuid4
3. 如果有密码则加密存储
4. 创建 Share 记录并保存
5. 返回分享信息
"""
http_exceptions.raise_not_implemented()
# 验证对象存在且属于当前用户
obj = await Object.get(session, Object.id == request.object_id)
if not obj or obj.owner_id != user.id:
raise HTTPException(status_code=404, detail="对象不存在或无权限")
# 生成分享码
code = str(uuid4())
# 密码加密处理(如果有)
hashed_password = None
if request.password:
hashed_password = Password.hash(request.password)
# 创建分享记录
share = Share(
code=code,
password=hashed_password,
object_id=request.object_id,
user_id=user.id,
expires=request.expires,
remain_downloads=request.remain_downloads,
preview_enabled=request.preview_enabled,
score=request.score,
source_name=obj.name,
)
share = await share.save(session)
l.info(f"用户 {user.id} 创建分享: {share.code}")
# 返回响应
return ShareResponse(
id=share.id,
code=share.code,
object_id=share.object_id,
source_name=share.source_name,
views=share.views,
downloads=share.downloads,
remain_downloads=share.remain_downloads,
expires=share.expires,
preview_enabled=share.preview_enabled,
score=share.score,
created_at=share.created_at,
is_expired=share.expires is not None and share.expires < datetime.now(),
has_password=share.password is not None,
)
@share_router.get(
path='/',
summary='列出我的分享',
description='Get a list of shares.',
dependencies=[Depends(auth_required)]
)
def router_share_list() -> ResponseBase:
async def router_share_list(
session: SessionDep,
user: Annotated[User, Depends(auth_required)],
offset: int = Query(default=0, ge=0),
limit: int = Query(default=50, le=100),
desc: bool = Query(default=True),
order: Literal["created_at", "updated_at"] = Query(default="created_at"),
keyword: str | None = Query(default=None),
expired: bool | None = Query(default=None),
) -> ListResponse[ShareResponse]:
"""
Get a list of shares.
Returns:
ResponseBase: A model containing the response data for the list of shares.
列出我的分享
认证:需要 JWT token
支持:
- 分页和排序
- 关键字搜索(搜索 source_name
- 过期状态筛选
"""
http_exceptions.raise_not_implemented()
# 构建基础条件
condition = Share.user_id == user.id
# 关键字搜索
if keyword:
condition = condition & Share.source_name.ilike(f"%{keyword}%")
# 过期状态筛选
now = datetime.now()
if expired is True:
# 已过期expires 不为 NULL 且 < 当前时间
condition = condition & (Share.expires != None) & (Share.expires < now)
elif expired is False:
# 未过期expires 为 NULL 或 >= 当前时间
condition = condition & ((Share.expires == None) | (Share.expires >= now))
# 构建 table_view
table_view = TableViewRequest(
offset=offset,
limit=limit,
desc=desc,
order=order,
)
# 使用 get_with_count 获取分页数据
result = await Share.get_with_count(
session,
condition,
table_view=table_view,
)
# 转换为响应模型
items = [
ShareResponse(
id=share.id,
code=share.code,
object_id=share.object_id,
source_name=share.source_name,
views=share.views,
downloads=share.downloads,
remain_downloads=share.remain_downloads,
expires=share.expires,
preview_enabled=share.preview_enabled,
score=share.score,
created_at=share.created_at,
is_expired=share.expires is not None and share.expires < now,
has_password=share.password is not None,
)
for share in result.items
]
return ListResponse(count=result.count, items=items)
@share_router.post(
path='/save/{id}',