新增查看站点概况
This commit is contained in:
@@ -90,4 +90,10 @@ from .model_base import (
|
||||
MCPRequestBase,
|
||||
MCPResponseBase,
|
||||
ResponseBase,
|
||||
# Admin Summary DTO
|
||||
MetricsSummary,
|
||||
LicenseInfo,
|
||||
VersionInfo,
|
||||
AdminSummaryData,
|
||||
AdminSummaryResponse,
|
||||
)
|
||||
@@ -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 方法枚举"""
|
||||
|
||||
|
||||
@@ -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
|
||||
"""是否有密码"""
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}',
|
||||
|
||||
Reference in New Issue
Block a user