mirror of
https://git.oceanpay.cc/danial/kami_apple_exchage.git
synced 2025-12-18 22:29:09 +00:00
feat(links): 实现基于权重的轮询算法和链接管理功能
- 新增链接权重字段,支持1-100范围设置 - 修改轮询算法为基于权重的选择机制 - 更新链接API接口返回统一使用LinkInfo模型 - 添加更新链接权重的PATCH端点 - 调整链接仓库查询逻辑,只包含激活状态链接 - 迁移链接相关Pydantic模型到task模块统一管理 - 修改分页响应格式为通用PaginatedResponse包装 - 禁用OpenTelemetry监控配置
This commit is contained in:
10
backend/.env
10
backend/.env
@@ -54,15 +54,15 @@ LOG_MAX_SIZE=10485760
|
||||
LOG_BACKUP_COUNT=5
|
||||
|
||||
# OpenTelemetry简化配置
|
||||
OTEL_ENABLED=true
|
||||
OTEL_SERVICE_NAME=apple-exchange-backend
|
||||
OTEL_ENABLED=false
|
||||
OTEL_SERVICE_NAME=苹果官网下单
|
||||
OTEL_SERVICE_VERSION=2.0.0
|
||||
OTEL_EXPORTER_ENDPOINT=http://38.38.251.113:31547
|
||||
OTEL_EXPORTER_PROTOCOL=grpc
|
||||
OTEL_EXPORTER_TIMEOUT=30
|
||||
OTEL_TRACES_ENABLED=true
|
||||
OTEL_METRICS_ENABLED=true
|
||||
OTEL_LOGS_ENABLED=true
|
||||
OTEL_TRACES_ENABLED=false
|
||||
OTEL_METRICS_ENABLED=false
|
||||
OTEL_LOGS_ENABLED=false
|
||||
OTEL_SAMPLER_RATIO=1.0
|
||||
OTEL_BATCH_SIZE=512
|
||||
OTEL_EXPORT_INTERVAL=5000
|
||||
|
||||
@@ -9,9 +9,12 @@ from app.core.database import get_async_db
|
||||
from app.core.log import get_logger
|
||||
from app.schemas.link import (
|
||||
LinkCreate,
|
||||
LinkListResponse,
|
||||
LinkResponse,
|
||||
LinkStatus,
|
||||
LinkUpdate,
|
||||
)
|
||||
from app.schemas.task import (
|
||||
LinkInfo,
|
||||
PaginatedResponse,
|
||||
)
|
||||
from app.services.link_service import LinksService
|
||||
|
||||
@@ -24,10 +27,10 @@ def get_link_service(db: AsyncSession = Depends(get_async_db)) -> LinksService:
|
||||
return LinksService(db)
|
||||
|
||||
|
||||
@router.post("/", response_model=LinkResponse)
|
||||
@router.post("/", response_model=LinkInfo)
|
||||
async def create_link(
|
||||
link_data: LinkCreate, link_service: LinksService = Depends(get_link_service)
|
||||
):
|
||||
) -> LinkInfo:
|
||||
"""创建新链接"""
|
||||
try:
|
||||
return await link_service.create_link(link_data)
|
||||
@@ -41,7 +44,7 @@ async def create_link(
|
||||
raise HTTPException(status_code=500, detail="创建链接失败")
|
||||
|
||||
|
||||
@router.get("/list", response_model=LinkListResponse)
|
||||
@router.get("/list", response_model=PaginatedResponse[LinkInfo])
|
||||
async def get_links(
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
size: int = Query(20, ge=1, le=1000, description="每页大小"),
|
||||
@@ -49,7 +52,7 @@ async def get_links(
|
||||
max_amount: float | None = Query(None, description="最大金额"),
|
||||
url_pattern: str | None = Query(None, description="URL模式"),
|
||||
link_service: LinksService = Depends(get_link_service),
|
||||
):
|
||||
) -> PaginatedResponse[LinkInfo]:
|
||||
"""获取链接列表"""
|
||||
try:
|
||||
result = await link_service.get_links(
|
||||
@@ -66,10 +69,10 @@ async def get_links(
|
||||
raise HTTPException(status_code=500, detail="获取链接列表失败")
|
||||
|
||||
|
||||
@router.get("/{link_id}", response_model=LinkResponse)
|
||||
@router.get("/{link_id}", response_model=LinkInfo)
|
||||
async def get_link(
|
||||
link_id: int, link_service: LinksService = Depends(get_link_service)
|
||||
):
|
||||
) -> LinkInfo:
|
||||
"""获取单个链接详情"""
|
||||
try:
|
||||
link = await link_service.get_link(str(link_id))
|
||||
@@ -87,7 +90,7 @@ async def get_link(
|
||||
@router.delete("/{link_id}")
|
||||
async def delete_link(
|
||||
link_id: str, link_service: LinksService = Depends(get_link_service)
|
||||
):
|
||||
) -> dict[str, str]:
|
||||
"""删除链接"""
|
||||
try:
|
||||
success = await link_service.delete_link(link_id)
|
||||
@@ -108,7 +111,7 @@ async def toggle_link_status(
|
||||
link_id: str,
|
||||
status: LinkStatus,
|
||||
link_service: LinksService = Depends(get_link_service),
|
||||
):
|
||||
) -> LinkInfo:
|
||||
"""切换链接状态"""
|
||||
try:
|
||||
updated_link = await link_service.update_link_status(link_id, status)
|
||||
@@ -122,3 +125,25 @@ async def toggle_link_status(
|
||||
except Exception as e:
|
||||
logger.error(f"切换链接状态失败: {str(e)}", link_id=link_id, exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="切换链接状态失败")
|
||||
|
||||
|
||||
@router.patch("/{link_id}/weight")
|
||||
async def update_link_weight(
|
||||
link_id: str,
|
||||
weight: int = Query(..., ge=1, le=100, description="权重值(1-100)"),
|
||||
link_service: LinksService = Depends(get_link_service),
|
||||
) -> LinkInfo:
|
||||
"""更新链接权重"""
|
||||
try:
|
||||
link_update = LinkUpdate(url=None, amount=None, weight=weight, status=None)
|
||||
updated_link = await link_service.update_link(link_id, link_update)
|
||||
if not updated_link:
|
||||
logger.warning(f"更新链接权重失败 - 链接不存在: {link_id}")
|
||||
raise HTTPException(status_code=404, detail="链接不存在")
|
||||
logger.info(f"链接权重更新成功: {link_id} -> {weight}")
|
||||
return updated_link
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"更新链接权重失败: {str(e)}", link_id=link_id, exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="更新链接权重失败")
|
||||
|
||||
@@ -12,13 +12,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.database import get_async_db
|
||||
from app.core.log import get_logger
|
||||
from app.models.orders import OrderStatus
|
||||
from app.schemas.link import LinkResponse
|
||||
from app.schemas.order import (
|
||||
OrderDetailResponse,
|
||||
OrderStatsResponse,
|
||||
)
|
||||
from app.schemas.task import CardInfo, UserInfo, LinkInfo
|
||||
from app.schemas.user_data import UserInfoResponse
|
||||
from app.services.order_business_service import OrderService
|
||||
|
||||
router = APIRouter()
|
||||
@@ -56,7 +54,8 @@ async def get_orders(
|
||||
# Convert SQLAlchemy models to Pydantic response schemas
|
||||
result = []
|
||||
for order in orders:
|
||||
link_response = LinkResponse(
|
||||
link_response = LinkInfo(
|
||||
weight=order.links.weight,
|
||||
id=order.links.id,
|
||||
url=order.links.url,
|
||||
amount=order.links.amount,
|
||||
@@ -79,7 +78,7 @@ async def get_orders(
|
||||
)
|
||||
)
|
||||
|
||||
user_data = UserInfoResponse(
|
||||
user_data = UserInfo(
|
||||
id=order.user_data.id,
|
||||
email=order.user_data.email,
|
||||
phone=order.user_data.phone,
|
||||
|
||||
@@ -9,10 +9,13 @@ from app.core.database import get_async_db
|
||||
from app.core.log import get_logger
|
||||
from app.schemas.user_data import (
|
||||
UserDataCreate,
|
||||
UserDataListResponse,
|
||||
UserDataResponse,
|
||||
UserDataUploadResponse,
|
||||
)
|
||||
from app.schemas.task import (
|
||||
BulkDeleteUserDataResponse,
|
||||
PaginatedResponse,
|
||||
)
|
||||
from app.services.user_data_service import UserDataService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -79,6 +82,30 @@ async def get_user_data(
|
||||
raise HTTPException(status_code=500, detail="获取用户数据详情失败")
|
||||
|
||||
|
||||
@router.delete("/all", response_model=BulkDeleteUserDataResponse, summary="批量删除所有用户数据")
|
||||
async def bulk_delete_all_user_data(
|
||||
skip_orders: bool = Query(False, description="是否跳过有关联订单的用户数据"),
|
||||
service: UserDataService = Depends(get_user_data_service),
|
||||
):
|
||||
"""
|
||||
批量软删除所有用户数据
|
||||
|
||||
- **skip_orders**: 是否跳过有关联订单的用户数据(默认false,会删除包括有关联订单的所有数据)
|
||||
|
||||
返回删除统计信息,包括总用户数、删除用户数和跳过用户数
|
||||
"""
|
||||
try:
|
||||
result = await service.delete_all_user_data(skip_orders=skip_orders)
|
||||
logger.info(
|
||||
f"批量删除用户数据完成: total={result['total_users']}, "
|
||||
f"deleted={result['deleted_users']}, skipped={result['skipped_users']}"
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"批量删除用户数据失败: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="批量删除用户数据失败")
|
||||
|
||||
|
||||
@router.delete("/{user_id}", summary="删除用户数据")
|
||||
async def delete_user_data(
|
||||
user_id: str, service: UserDataService = Depends(get_user_data_service)
|
||||
@@ -105,7 +132,7 @@ async def delete_user_data(
|
||||
raise HTTPException(status_code=500, detail="删除用户数据失败")
|
||||
|
||||
|
||||
@router.get("/list", response_model=UserDataListResponse, summary="获取用户数据列表")
|
||||
@router.get("/list", response_model=PaginatedResponse[UserDataResponse], summary="获取用户数据列表")
|
||||
async def get_user_data_list(
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
size: int = Query(20, ge=1, le=100, description="每页大小"),
|
||||
@@ -202,3 +229,5 @@ async def batch_upload_user_data(
|
||||
except Exception as e:
|
||||
logger.error(f"批量上传用户数据失败: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="批量上传用户数据失败")
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from sqlalchemy.ext.asyncio import (
|
||||
async_sessionmaker,
|
||||
create_async_engine,
|
||||
)
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import Boolean, Float, Index, String
|
||||
from sqlalchemy import Boolean, Float, Index, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import BaseModel
|
||||
@@ -33,6 +33,7 @@ class Links(BaseModel):
|
||||
|
||||
url: Mapped[str] = mapped_column(String(255), nullable=False, comment="链接地址")
|
||||
amount: Mapped[float] = mapped_column(Float, nullable=False, comment="金额")
|
||||
weight: Mapped[int] = mapped_column(Integer, default=1, nullable=False, comment="权重(1-100)")
|
||||
status: Mapped[LinkStatus] = mapped_column(
|
||||
default=LinkStatus.ACTIVE, comment="链接状态"
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.log import get_logger
|
||||
from app.models.links import Links
|
||||
from app.models.links import LinkStatus, Links
|
||||
from app.repositories.base_repository import BaseRepository
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -142,28 +142,55 @@ class LinkRepository(BaseRepository[Links]):
|
||||
self, current_position: int = 0
|
||||
) -> tuple[Links, int] | None:
|
||||
"""
|
||||
从轮询池中获取下一个链接
|
||||
从轮询池中获取下一个链接(基于权重的轮询算法)
|
||||
|
||||
Args:
|
||||
current_position: 当前位置
|
||||
current_position: 当前轮询位置
|
||||
|
||||
Returns:
|
||||
(链接实例, 新位置) 或 None
|
||||
"""
|
||||
# 获取所有链接按ID排序(确保顺序一致)
|
||||
query = select(Links).order_by(Links.id)
|
||||
# 获取所有激活状态且未软删除的链接,按权重降序排列
|
||||
query = select(Links).where(
|
||||
Links.status == LinkStatus.ACTIVE.value,
|
||||
Links.is_deleted == False
|
||||
).order_by(Links.weight.desc())
|
||||
result = await self.db_session.execute(query)
|
||||
links = list(result.scalars().all())
|
||||
|
||||
if not links:
|
||||
return None
|
||||
|
||||
# 计算下一个位置
|
||||
next_position = (current_position + 1) % len(links)
|
||||
next_link = links[next_position]
|
||||
# 计算总权重
|
||||
total_weight = sum(link.weight for link in links)
|
||||
if total_weight == 0:
|
||||
return None
|
||||
|
||||
logger.info(f"从轮询池获取链接: {next_link.id}, 位置: {next_position}")
|
||||
return next_link, next_position
|
||||
# 使用基于权重的轮询算法
|
||||
# current_position 在总权重范围内循环,确保权重越高的链接被选中的频率越高
|
||||
position_in_weight_cycle = current_position % total_weight
|
||||
|
||||
# 遍历链接,累积权重直到找到应该被选中的链接
|
||||
accumulated_weight = 0
|
||||
|
||||
for link in links:
|
||||
accumulated_weight += link.weight
|
||||
if position_in_weight_cycle < accumulated_weight:
|
||||
# 计算下一个位置,确保在总权重范围内循环
|
||||
next_position = (current_position + 1) % total_weight if total_weight > 0 else 0
|
||||
logger.info(
|
||||
f"从权重轮询池获取链接: {link.id}, 权重: {link.weight}, "
|
||||
f"累积权重: {accumulated_weight}, 位置: {next_position}"
|
||||
)
|
||||
return link, next_position
|
||||
|
||||
# 理论上不应该到达这里,但作为保险措施
|
||||
next_position = (current_position + 1) % total_weight if total_weight > 0 else 0
|
||||
selected_link = links[0]
|
||||
logger.info(
|
||||
f"从权重轮询池获取链接(默认): {selected_link.id}, 权重: {selected_link.weight}, 位置: {next_position}"
|
||||
)
|
||||
return selected_link, next_position
|
||||
|
||||
async def get_link_by_pool_position(self, position: int) -> Links | None:
|
||||
"""
|
||||
@@ -181,9 +208,14 @@ class LinkRepository(BaseRepository[Links]):
|
||||
|
||||
async def get_pool_size(self) -> int:
|
||||
"""
|
||||
获取轮询池大小
|
||||
获取轮询池大小(仅包含激活状态且未软删除的链接)
|
||||
|
||||
Returns:
|
||||
池中链接总数
|
||||
"""
|
||||
return await self.count()
|
||||
query = select(func.count()).where(
|
||||
Links.status == LinkStatus.ACTIVE.value,
|
||||
Links.is_deleted == False
|
||||
)
|
||||
result = await self.db_session.execute(query)
|
||||
return result.scalar() or 0
|
||||
|
||||
@@ -130,3 +130,61 @@ class UserDataRepository(BaseRepository[UserData]):
|
||||
# 创建新用户
|
||||
new_user = await self.create(**user_data)
|
||||
return new_user, True
|
||||
|
||||
async def bulk_soft_delete(self, skip_orders: bool = False) -> dict[str, any]:
|
||||
"""
|
||||
批量软删除用户数据
|
||||
|
||||
Args:
|
||||
skip_orders: 是否跳过有关联订单的用户数据
|
||||
|
||||
Returns:
|
||||
删除统计信息
|
||||
"""
|
||||
try:
|
||||
# 获取所有未删除的用户数据
|
||||
all_users = await self.get_all(include_deleted=False)
|
||||
|
||||
if not all_users:
|
||||
return {
|
||||
"total_users": 0,
|
||||
"deleted_users": 0,
|
||||
"skipped_users": 0,
|
||||
"message": "没有找到需要删除的用户数据"
|
||||
}
|
||||
|
||||
deleted_count = 0
|
||||
skipped_count = 0
|
||||
user_ids_to_delete = []
|
||||
|
||||
for user in all_users:
|
||||
# 检查是否有关联订单
|
||||
if skip_orders:
|
||||
user_with_orders = await self.get_user_with_orders(user.id)
|
||||
if user_with_orders and user_with_orders.orders:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
user_ids_to_delete.append(user.id)
|
||||
deleted_count += 1
|
||||
|
||||
# 批量执行软删除
|
||||
if user_ids_to_delete:
|
||||
await self.bulk_delete(user_ids_to_delete)
|
||||
|
||||
logger.info(
|
||||
f"批量软删除用户数据完成: total={len(all_users)}, "
|
||||
f"deleted={deleted_count}, skipped={skipped_count}"
|
||||
)
|
||||
|
||||
return {
|
||||
"total_users": len(all_users),
|
||||
"deleted_users": deleted_count,
|
||||
"skipped_users": skipped_count,
|
||||
"message": f"成功软删除 {deleted_count} 个用户数据" +
|
||||
(f",跳过 {skipped_count} 个有关联订单的用户数据" if skipped_count > 0 else "")
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"批量软删除用户数据失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""
|
||||
Pydantic schemas for API request/response models
|
||||
统一管理的所有 Pydantic 模型
|
||||
"""
|
||||
|
||||
from .health import (
|
||||
@@ -8,39 +9,45 @@ from .health import (
|
||||
LivenessCheckResponse,
|
||||
ReadinessCheckResponse,
|
||||
)
|
||||
from .link import (
|
||||
LinkCreate,
|
||||
LinkListResponse,
|
||||
LinkPoolResponse,
|
||||
LinkResponse,
|
||||
LinkStatsResponse,
|
||||
LinkUpdate,
|
||||
)
|
||||
from .order import (
|
||||
OrderDetailResponse,
|
||||
OrderStatsResponse,
|
||||
UploadUrlRequest,
|
||||
UploadUrlResponse,
|
||||
)
|
||||
from .task import (
|
||||
BatchProcessRequest,
|
||||
CardInfo,
|
||||
DeleteAllDataResponse,
|
||||
GiftCardDetailCreate,
|
||||
GiftCardInfoCreate,
|
||||
GiftCardInfoResponse,
|
||||
GiftCardRequest,
|
||||
GiftCardResponse,
|
||||
GiftCardSubmissionRequest,
|
||||
GiftCardSubmissionResponse,
|
||||
LinkBase,
|
||||
LinkCreate,
|
||||
LinkInfo,
|
||||
LinkPoolResponse,
|
||||
LinkStatsResponse,
|
||||
LinkStatus,
|
||||
LinkUpdate,
|
||||
OrderDetailResponse,
|
||||
OrderStatsResponse,
|
||||
PaginatedResponse,
|
||||
ProcessOrderRequest,
|
||||
QueueStatsResponse,
|
||||
TaskControlRequest,
|
||||
TaskControlResponse,
|
||||
TaskListResponse,
|
||||
TaskListItem,
|
||||
TaskStateResponse,
|
||||
TaskStatusResponse,
|
||||
UploadUrlRequest,
|
||||
UploadUrlResponse,
|
||||
UserInfo,
|
||||
WorkerStatusResponse,
|
||||
)
|
||||
from .user_data import (
|
||||
UserDataBase,
|
||||
UserDataCreate,
|
||||
UserDataListResponse,
|
||||
UserDataResponse,
|
||||
UserDataStatsResponse,
|
||||
UserDataUpdate,
|
||||
UserDataUploadResponse,
|
||||
WorkerStatusResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -49,32 +56,45 @@ __all__ = [
|
||||
"LinkInfo",
|
||||
"CardInfo",
|
||||
"PaginatedResponse",
|
||||
"LinkStatus",
|
||||
# User data schemas
|
||||
"UserDataBase",
|
||||
"UserDataCreate",
|
||||
"UserDataUpdate",
|
||||
"UserDataResponse",
|
||||
"UserDataUploadResponse",
|
||||
"UserDataStatsResponse",
|
||||
# Link schemas
|
||||
"LinkBase",
|
||||
"LinkCreate",
|
||||
"LinkUpdate",
|
||||
"LinkPoolResponse",
|
||||
"LinkStatsResponse",
|
||||
# Order schemas
|
||||
"OrderStatsResponse",
|
||||
"OrderDetailResponse",
|
||||
"UploadUrlRequest",
|
||||
"UploadUrlResponse",
|
||||
# Gift card schemas
|
||||
"GiftCardRequest",
|
||||
"GiftCardResponse",
|
||||
"GiftCardSubmissionRequest",
|
||||
"GiftCardSubmissionResponse",
|
||||
"GiftCardInfoCreate",
|
||||
"GiftCardInfoResponse",
|
||||
"GiftCardDetailCreate",
|
||||
# Task schemas
|
||||
"ProcessOrderRequest",
|
||||
"BatchProcessRequest",
|
||||
"TaskStatusResponse",
|
||||
"WorkerStatusResponse",
|
||||
"QueueStatsResponse",
|
||||
# Link schemas
|
||||
"LinkCreate",
|
||||
"LinkUpdate",
|
||||
"LinkResponse",
|
||||
"LinkListResponse",
|
||||
"LinkPoolResponse",
|
||||
"LinkStatsResponse",
|
||||
# User data schemas
|
||||
"UserDataCreate",
|
||||
"UserDataUpdate",
|
||||
"UserDataResponse",
|
||||
"UserDataListResponse",
|
||||
"UserDataUploadResponse",
|
||||
"UserDataStatsResponse",
|
||||
"TaskControlRequest",
|
||||
"TaskControlResponse",
|
||||
"TaskStateResponse",
|
||||
"TaskListResponse",
|
||||
"TaskListItem",
|
||||
"DeleteAllDataResponse",
|
||||
# Health schemas
|
||||
"HealthCheckResponse",
|
||||
"DetailedHealthCheckResponse",
|
||||
|
||||
@@ -21,5 +21,3 @@ __all__ = [
|
||||
"GiftCardDetailCreate",
|
||||
]
|
||||
|
||||
# 为了向后兼容,保留别名
|
||||
GiftCardInfoResponse = CardInfo
|
||||
|
||||
@@ -1,103 +1,27 @@
|
||||
"""
|
||||
链接相关的Pydantic模型
|
||||
已迁移到 app.schemas.task 模块中统一管理
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
# 从统一schema导入所有链接相关模型
|
||||
from app.schemas.task import (
|
||||
LinkBase,
|
||||
LinkCreate,
|
||||
LinkInfo,
|
||||
LinkPoolResponse,
|
||||
LinkStatsResponse,
|
||||
LinkUpdate,
|
||||
LinkStatus,
|
||||
PaginatedResponse,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class LinkStatus(str, Enum):
|
||||
"""链接状态枚举"""
|
||||
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
|
||||
|
||||
class LinkBase(BaseModel):
|
||||
"""链接基础模型"""
|
||||
|
||||
url: str = Field(..., description="链接URL", max_length=255)
|
||||
amount: float = Field(..., description="金额", gt=0)
|
||||
status: LinkStatus = Field(LinkStatus.ACTIVE, description="链接状态")
|
||||
|
||||
|
||||
class LinkCreate(LinkBase):
|
||||
"""创建链接请求模型"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class LinkUpdate(BaseModel):
|
||||
"""更新链接请求模型"""
|
||||
|
||||
url: str | None = Field(None, description="链接URL", max_length=255)
|
||||
amount: float | None = Field(None, description="金额", gt=0)
|
||||
status: LinkStatus | None = Field(None, description="链接状态")
|
||||
|
||||
|
||||
# 从统一schema导入LinkInfo(延迟导入避免循环导入)
|
||||
try:
|
||||
from app.schemas.task import LinkInfo
|
||||
|
||||
# 为了向后兼容,保留别名
|
||||
LinkResponse = LinkInfo
|
||||
except ImportError:
|
||||
# 如果导入失败,保持原有的LinkResponse
|
||||
class LinkResponse(LinkBase):
|
||||
"""链接响应模型"""
|
||||
|
||||
id: str = Field(..., description="链接ID")
|
||||
created_at: str = Field(..., description="创建时间")
|
||||
updated_at: str = Field(..., description="更新时间")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def from_orm(cls, obj):
|
||||
"""Custom ORM conversion to handle datetime serialization"""
|
||||
data = {}
|
||||
for field in cls.model_fields:
|
||||
value = getattr(obj, field, None)
|
||||
if isinstance(value, datetime):
|
||||
data[field] = value.isoformat()
|
||||
else:
|
||||
data[field] = value
|
||||
|
||||
return cls(**data)
|
||||
|
||||
|
||||
# 从统一schema导入PaginatedResponse(延迟导入避免循环导入)
|
||||
try:
|
||||
from app.schemas.task import PaginatedResponse
|
||||
|
||||
# 为了向后兼容,保留别名
|
||||
LinkListResponse = PaginatedResponse[LinkResponse]
|
||||
except ImportError:
|
||||
# 如果导入失败,保持原有的LinkListResponse
|
||||
class LinkListResponse(BaseModel):
|
||||
"""链接列表响应模型"""
|
||||
|
||||
items: list[LinkResponse]
|
||||
total: int
|
||||
page: int
|
||||
size: int
|
||||
pages: int
|
||||
|
||||
|
||||
class LinkPoolResponse(BaseModel):
|
||||
"""轮询池响应模型"""
|
||||
|
||||
link: LinkResponse
|
||||
pool_position: int = Field(..., description="在轮询池中的位置")
|
||||
|
||||
|
||||
class LinkStatsResponse(BaseModel):
|
||||
"""链接统计响应模型"""
|
||||
|
||||
total_links: int = Field(..., description="总链接数")
|
||||
total_orders: int = Field(..., description="总订单数")
|
||||
average_amount: float = Field(..., description="平均金额")
|
||||
min_amount: float = Field(..., description="最小金额")
|
||||
max_amount: float = Field(..., description="最大金额")
|
||||
__all__ = [
|
||||
"LinkBase",
|
||||
"LinkCreate",
|
||||
"LinkInfo",
|
||||
"LinkPoolResponse",
|
||||
"LinkStatsResponse",
|
||||
"LinkUpdate",
|
||||
"LinkStatus",
|
||||
"PaginatedResponse",
|
||||
]
|
||||
|
||||
@@ -1,56 +1,20 @@
|
||||
"""
|
||||
订单相关的Pydantic模型
|
||||
已迁移到 app.schemas.task 模块中统一管理
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
# 从统一schema导入所有订单相关模型
|
||||
from app.schemas.task import (
|
||||
OrderDetailResponse,
|
||||
OrderStatsResponse,
|
||||
UploadUrlRequest,
|
||||
UploadUrlResponse,
|
||||
UserInfo,
|
||||
)
|
||||
|
||||
from app.models.orders import OrderStatus
|
||||
from app.schemas.task import CardInfo, LinkInfo, UserInfo
|
||||
|
||||
|
||||
class OrderStatsResponse(BaseModel):
|
||||
"""订单统计响应"""
|
||||
|
||||
total: int
|
||||
pending: int
|
||||
processing: int
|
||||
success: int
|
||||
failed: int
|
||||
last_update: str
|
||||
|
||||
|
||||
class OrderDetailResponse(BaseModel):
|
||||
"""订单详情响应 - 与数据库结构完全一致"""
|
||||
|
||||
id: str = Field(..., description="订单ID")
|
||||
status: OrderStatus = Field(..., description="订单状态")
|
||||
created_at: str = Field(..., description="创建时间")
|
||||
updated_at: str = Field(..., description="更新时间")
|
||||
final_order_url: str | None = Field(None, description="最终订单URL")
|
||||
final_order_id: str | None = Field(None, description="最终苹果订单ID")
|
||||
failure_reason: str | None = Field(None, description="失败原因")
|
||||
user_data_id: str = Field(..., description="用户数据ID")
|
||||
links_id: str = Field(..., description="链接ID")
|
||||
|
||||
# 关联关系
|
||||
user_data: UserInfo = Field(description="用户数据")
|
||||
links: LinkInfo = Field(description="链接信息")
|
||||
gift_cards: list[CardInfo] = Field(default_factory=list, description="礼品卡列表")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class UploadUrlRequest(BaseModel):
|
||||
"""上传URL请求"""
|
||||
|
||||
url: str = Field(..., min_length=1, description="上传URL")
|
||||
thread_id: str | None = Field(None, description="线程ID")
|
||||
|
||||
|
||||
class UploadUrlResponse(BaseModel):
|
||||
"""上传URL响应"""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
upload_config_id: str
|
||||
url: str
|
||||
__all__ = [
|
||||
"OrderDetailResponse",
|
||||
"OrderStatsResponse",
|
||||
"UploadUrlRequest",
|
||||
"UploadUrlResponse",
|
||||
]
|
||||
|
||||
@@ -4,14 +4,21 @@
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Generic, List, TypeVar
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from app.enums.task import OrderTaskStatus
|
||||
from app.models.orders import OrderStatus
|
||||
from app.models.giftcards import GiftCardStatus
|
||||
from app.models.links import LinkStatus
|
||||
|
||||
|
||||
class LinkStatus(str, Enum):
|
||||
"""链接状态枚举"""
|
||||
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -101,8 +108,6 @@ class UserInfo(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# 为了向后兼容,保留 TaskUserInfo 别名
|
||||
TaskUserInfo = UserInfo
|
||||
|
||||
|
||||
class LinkInfo(BaseModel):
|
||||
@@ -111,6 +116,7 @@ class LinkInfo(BaseModel):
|
||||
id: str = Field(..., description="链接ID")
|
||||
url: str = Field(..., description="链接地址")
|
||||
amount: float = Field(..., description="金额")
|
||||
weight: int = Field(..., description="权重(1-100)")
|
||||
status: LinkStatus = Field(..., description="链接状态")
|
||||
created_at: str = Field(description="创建时间")
|
||||
updated_at: str = Field(description="更新时间")
|
||||
@@ -118,8 +124,6 @@ class LinkInfo(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# 为了向后兼容,保留 TaskLinkInfo 别名
|
||||
TaskLinkInfo = LinkInfo
|
||||
|
||||
|
||||
class CardInfo(BaseModel):
|
||||
@@ -137,8 +141,6 @@ class CardInfo(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# 为了向后兼容,保留 TaskCardInfo 别名
|
||||
TaskCardInfo = CardInfo
|
||||
|
||||
|
||||
class TaskListItem(BaseModel):
|
||||
@@ -268,3 +270,173 @@ class PaginatedResponse(BaseModel, Generic[T]):
|
||||
pages: int = Field(..., description="总页数")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
# 用户数据相关模型
|
||||
class UserDataBase(BaseModel):
|
||||
"""用户数据基础模型"""
|
||||
|
||||
first_name: str = Field(..., description="名字", max_length=255)
|
||||
last_name: str = Field(..., description="姓氏", max_length=255)
|
||||
email: str = Field(..., description="邮箱", max_length=255)
|
||||
phone: str = Field(..., description="电话", max_length=50)
|
||||
street_address: str = Field(..., description="街道地址", max_length=500)
|
||||
city: str = Field(..., description="城市", max_length=255)
|
||||
state: str = Field(..., description="州/省", max_length=255)
|
||||
zip_code: str = Field(..., description="邮编", max_length=20)
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def validate_email(cls, v):
|
||||
"""验证邮箱格式"""
|
||||
import re
|
||||
|
||||
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
||||
if not re.match(pattern, v):
|
||||
raise ValueError("邮箱格式不正确")
|
||||
return v
|
||||
|
||||
|
||||
class UserDataCreate(UserDataBase):
|
||||
"""创建用户数据请求模型"""
|
||||
pass
|
||||
|
||||
|
||||
class UserDataUpdate(BaseModel):
|
||||
"""更新用户数据请求模型"""
|
||||
|
||||
first_name: str | None = Field(None, description="名字", max_length=255)
|
||||
last_name: str | None = Field(None, description="姓氏", max_length=255)
|
||||
email: str | None = Field(None, description="邮箱", max_length=255)
|
||||
phone: str | None = Field(None, description="电话", max_length=50)
|
||||
street_address: str | None = Field(None, description="街道地址", max_length=500)
|
||||
city: str | None = Field(None, description="城市", max_length=255)
|
||||
state: str | None = Field(None, description="州/省", max_length=255)
|
||||
zip_code: str | None = Field(None, description="邮编", max_length=20)
|
||||
|
||||
|
||||
class UserDataResponse(UserDataBase):
|
||||
"""用户数据响应模型"""
|
||||
|
||||
id: str = Field(..., description="用户数据ID")
|
||||
created_at: str = Field(..., description="创建时间")
|
||||
updated_at: str = Field(..., description="更新时间")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class UserDataUploadResponse(BaseModel):
|
||||
"""用户数据上传响应模型"""
|
||||
|
||||
user_data: UserDataResponse
|
||||
message: str = Field(..., description="响应消息")
|
||||
|
||||
|
||||
class UserDataStatsResponse(BaseModel):
|
||||
"""用户数据统计响应模型"""
|
||||
|
||||
total_users: int = Field(..., description="总用户数")
|
||||
total_orders: int = Field(..., description="总订单数")
|
||||
recent_uploads: int = Field(..., description="最近上传数量")
|
||||
success_rate: float = Field(..., description="成功率")
|
||||
|
||||
|
||||
class BulkDeleteUserDataResponse(BaseModel):
|
||||
"""批量删除用户数据响应模型"""
|
||||
|
||||
total_users: int = Field(..., description="总用户数")
|
||||
deleted_users: int = Field(..., description="已删除用户数")
|
||||
skipped_users: int = Field(..., description="跳过的用户数")
|
||||
message: str = Field(..., description="响应消息")
|
||||
|
||||
|
||||
# 链接相关模型
|
||||
class LinkBase(BaseModel):
|
||||
"""链接基础模型"""
|
||||
|
||||
url: str = Field(..., description="链接URL", max_length=255)
|
||||
amount: float = Field(..., description="金额", gt=0)
|
||||
weight: int = Field(1, description="权重(1-100)", ge=1, le=100)
|
||||
status: LinkStatus = Field(LinkStatus.ACTIVE, description="链接状态")
|
||||
|
||||
|
||||
class LinkCreate(LinkBase):
|
||||
"""创建链接请求模型"""
|
||||
pass
|
||||
|
||||
|
||||
class LinkUpdate(BaseModel):
|
||||
"""更新链接请求模型"""
|
||||
|
||||
url: str | None = Field(None, description="链接URL", max_length=255)
|
||||
amount: float | None = Field(None, description="金额", gt=0)
|
||||
weight: int | None = Field(None, description="权重(1-100)", ge=1, le=100)
|
||||
status: LinkStatus | None = Field(None, description="链接状态")
|
||||
|
||||
model_config = ConfigDict(use_enum_values=True)
|
||||
|
||||
|
||||
class LinkPoolResponse(BaseModel):
|
||||
"""轮询池响应模型"""
|
||||
|
||||
link: LinkInfo
|
||||
pool_position: int = Field(..., description="在轮询池中的位置")
|
||||
|
||||
|
||||
class LinkStatsResponse(BaseModel):
|
||||
"""链接统计响应模型"""
|
||||
|
||||
total_links: int = Field(..., description="总链接数")
|
||||
total_orders: int = Field(..., description="总订单数")
|
||||
average_amount: float = Field(..., description="平均金额")
|
||||
min_amount: float = Field(..., description="最小金额")
|
||||
max_amount: float = Field(..., description="最大金额")
|
||||
|
||||
|
||||
# 订单相关模型
|
||||
class OrderStatsResponse(BaseModel):
|
||||
"""订单统计响应"""
|
||||
|
||||
total: int
|
||||
pending: int
|
||||
processing: int
|
||||
success: int
|
||||
failed: int
|
||||
last_update: str
|
||||
|
||||
|
||||
class OrderDetailResponse(BaseModel):
|
||||
"""订单详情响应 - 与数据库结构完全一致"""
|
||||
|
||||
id: str = Field(..., description="订单ID")
|
||||
status: OrderStatus = Field(..., description="订单状态")
|
||||
created_at: str = Field(..., description="创建时间")
|
||||
updated_at: str = Field(..., description="更新时间")
|
||||
final_order_url: str | None = Field(None, description="最终订单URL")
|
||||
final_order_id: str | None = Field(None, description="最终苹果订单ID")
|
||||
failure_reason: str | None = Field(None, description="失败原因")
|
||||
user_data_id: str = Field(..., description="用户数据ID")
|
||||
links_id: str = Field(..., description="链接ID")
|
||||
|
||||
# 关联关系
|
||||
user_data: UserInfo = Field(description="用户数据")
|
||||
links: LinkInfo = Field(description="链接信息")
|
||||
gift_cards: list[CardInfo] = Field(default_factory=list, description="礼品卡列表")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class UploadUrlRequest(BaseModel):
|
||||
"""上传URL请求"""
|
||||
|
||||
url: str = Field(..., min_length=1, description="上传URL")
|
||||
thread_id: str | None = Field(None, description="线程ID")
|
||||
|
||||
|
||||
class UploadUrlResponse(BaseModel):
|
||||
"""上传URL响应"""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
upload_config_id: str
|
||||
url: str
|
||||
|
||||
@@ -1,104 +1,27 @@
|
||||
"""
|
||||
用户数据相关的Pydantic模型
|
||||
已迁移到 app.schemas.task 模块中统一管理
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
# 从统一schema导入所有用户数据相关模型
|
||||
from app.schemas.task import (
|
||||
UserDataBase,
|
||||
UserDataCreate,
|
||||
UserDataUpdate,
|
||||
UserDataResponse,
|
||||
UserDataUploadResponse,
|
||||
UserDataStatsResponse,
|
||||
PaginatedResponse,
|
||||
UserInfo,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class UserDataBase(BaseModel):
|
||||
"""用户数据基础模型"""
|
||||
|
||||
first_name: str = Field(..., description="名字", max_length=255)
|
||||
last_name: str = Field(..., description="姓氏", max_length=255)
|
||||
email: str = Field(..., description="邮箱", max_length=255)
|
||||
phone: str = Field(..., description="电话", max_length=50)
|
||||
street_address: str = Field(..., description="街道地址", max_length=500)
|
||||
city: str = Field(..., description="城市", max_length=255)
|
||||
state: str = Field(..., description="州/省", max_length=255)
|
||||
zip_code: str = Field(..., description="邮编", max_length=20)
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def validate_email(cls, v):
|
||||
"""验证邮箱格式"""
|
||||
import re
|
||||
|
||||
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
||||
if not re.match(pattern, v):
|
||||
raise ValueError("邮箱格式不正确")
|
||||
return v
|
||||
|
||||
|
||||
class UserDataCreate(UserDataBase):
|
||||
"""创建用户数据请求模型"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserDataUpdate(BaseModel):
|
||||
"""更新用户数据请求模型"""
|
||||
|
||||
first_name: str | None = Field(None, description="名字", max_length=255)
|
||||
last_name: str | None = Field(None, description="姓氏", max_length=255)
|
||||
email: str | None = Field(None, description="邮箱", max_length=255)
|
||||
phone: str | None = Field(None, description="电话", max_length=50)
|
||||
street_address: str | None = Field(None, description="街道地址", max_length=500)
|
||||
city: str | None = Field(None, description="城市", max_length=255)
|
||||
state: str | None = Field(None, description="州/省", max_length=255)
|
||||
zip_code: str | None = Field(None, description="邮编", max_length=20)
|
||||
|
||||
|
||||
class UserDataResponse(UserDataBase):
|
||||
"""用户数据响应模型"""
|
||||
|
||||
id: str = Field(..., description="用户数据ID")
|
||||
created_at: str = Field(..., description="创建时间")
|
||||
updated_at: str = Field(..., description="更新时间")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# 从统一schema导入PaginatedResponse
|
||||
from app.schemas.task import PaginatedResponse
|
||||
|
||||
# 为了向后兼容,保留别名
|
||||
UserDataListResponse = PaginatedResponse[UserDataResponse]
|
||||
|
||||
|
||||
class UserDataUploadResponse(BaseModel):
|
||||
"""用户数据上传响应模型"""
|
||||
|
||||
user_data: UserDataResponse
|
||||
message: str = Field(..., description="响应消息")
|
||||
|
||||
|
||||
class UserDataResponse(UserDataBase):
|
||||
"""用户数据响应模型"""
|
||||
|
||||
id: str = Field(..., description="用户数据ID")
|
||||
created_at: str = Field(..., description="创建时间")
|
||||
updated_at: str = Field(..., description="更新时间")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class UserDataStatsResponse(BaseModel):
|
||||
"""用户数据统计响应模型"""
|
||||
|
||||
total_users: int = Field(..., description="总用户数")
|
||||
total_orders: int = Field(..., description="总订单数")
|
||||
recent_uploads: int = Field(..., description="最近上传数量")
|
||||
success_rate: float = Field(..., description="成功率")
|
||||
|
||||
|
||||
# 从统一schema导入UserInfo(延迟导入避免循环导入)
|
||||
try:
|
||||
from app.schemas.task import UserInfo
|
||||
|
||||
# 为了向后兼容,保留别名
|
||||
UserInfoResponse = UserInfo
|
||||
except ImportError:
|
||||
# 如果导入失败,定义一个临时的UserInfoResponse
|
||||
UserInfoResponse = UserDataResponse
|
||||
__all__ = [
|
||||
"UserDataBase",
|
||||
"UserDataCreate",
|
||||
"UserDataUpdate",
|
||||
"UserDataResponse",
|
||||
"UserDataUploadResponse",
|
||||
"UserDataStatsResponse",
|
||||
"PaginatedResponse",
|
||||
"UserInfo",
|
||||
]
|
||||
|
||||
@@ -16,13 +16,15 @@ from app.models.links import Links
|
||||
from app.repositories.repository_factory import RepositoryFactory
|
||||
from app.schemas.link import (
|
||||
LinkCreate,
|
||||
LinkListResponse,
|
||||
LinkPoolResponse,
|
||||
LinkResponse,
|
||||
LinkStatsResponse,
|
||||
LinkUpdate,
|
||||
LinkStatus,
|
||||
)
|
||||
from app.schemas.task import (
|
||||
LinkInfo,
|
||||
PaginatedResponse,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -39,7 +41,7 @@ class LinksService:
|
||||
self.repo_factory = RepositoryFactory(db)
|
||||
# 注意:这里不再直接获取redis客户端,而是在需要时调用get_redis()
|
||||
|
||||
async def create_link(self, link_data: LinkCreate) -> LinkResponse:
|
||||
async def create_link(self, link_data: LinkCreate) -> LinkInfo:
|
||||
"""
|
||||
创建新链接
|
||||
|
||||
@@ -56,7 +58,7 @@ class LinksService:
|
||||
|
||||
# 创建链接
|
||||
link = await self.repo_factory.links.create(
|
||||
url=link_data.url, amount=link_data.amount
|
||||
url=link_data.url, amount=link_data.amount, weight=link_data.weight
|
||||
)
|
||||
|
||||
logger.info(f"创建链接成功: {link.id}")
|
||||
@@ -64,7 +66,7 @@ class LinksService:
|
||||
|
||||
async def update_link_status(
|
||||
self, link_id: str, status: LinkStatus
|
||||
) -> LinkResponse | None:
|
||||
) -> LinkInfo | None:
|
||||
"""
|
||||
更新链接状态
|
||||
|
||||
@@ -83,7 +85,7 @@ class LinksService:
|
||||
return self._convert_to_response(updated_link)
|
||||
return None
|
||||
|
||||
async def get_link(self, link_id: str) -> LinkResponse | None:
|
||||
async def get_link(self, link_id: str) -> LinkInfo | None:
|
||||
"""
|
||||
获取单个链接
|
||||
|
||||
@@ -100,7 +102,7 @@ class LinksService:
|
||||
|
||||
async def update_link(
|
||||
self, link_id: str, link_data: LinkUpdate
|
||||
) -> LinkResponse | None:
|
||||
) -> LinkInfo | None:
|
||||
"""
|
||||
更新链接
|
||||
|
||||
@@ -164,7 +166,7 @@ class LinksService:
|
||||
min_amount: float | None = None,
|
||||
max_amount: float | None = None,
|
||||
url_pattern: str | None = None,
|
||||
) -> LinkListResponse:
|
||||
) -> PaginatedResponse[LinkInfo]:
|
||||
"""
|
||||
获取链接列表
|
||||
|
||||
@@ -198,7 +200,7 @@ class LinksService:
|
||||
total = result.total
|
||||
pages = result.pages
|
||||
|
||||
return LinkListResponse(
|
||||
return PaginatedResponse[LinkInfo](
|
||||
items=[self._convert_to_response(link) for link in links],
|
||||
total=total,
|
||||
page=page,
|
||||
@@ -215,7 +217,7 @@ class LinksService:
|
||||
|
||||
page_links = sorted(page_links, key=lambda x: x.created_at, reverse=True)
|
||||
|
||||
return LinkListResponse(
|
||||
return PaginatedResponse[LinkInfo](
|
||||
items=[self._convert_to_response(link) for link in page_links],
|
||||
total=total,
|
||||
page=page,
|
||||
@@ -297,7 +299,7 @@ class LinksService:
|
||||
pool_size = await self.repo_factory.links.get_pool_size()
|
||||
await redis_client.set(self.POOL_SIZE_KEY, str(pool_size))
|
||||
|
||||
def _convert_to_response(self, link: Links) -> LinkResponse:
|
||||
def _convert_to_response(self, link: Links) -> LinkInfo:
|
||||
"""
|
||||
将链接模型转换为响应模型
|
||||
|
||||
@@ -307,10 +309,11 @@ class LinksService:
|
||||
Returns:
|
||||
链接响应模型
|
||||
"""
|
||||
return LinkResponse(
|
||||
return LinkInfo(
|
||||
id=link.id,
|
||||
url=link.url,
|
||||
amount=link.amount,
|
||||
weight=link.weight,
|
||||
status=link.status,
|
||||
created_at=link.created_at.isoformat(),
|
||||
updated_at=link.updated_at.isoformat(),
|
||||
|
||||
@@ -14,14 +14,14 @@ from app.core.state_manager import StateType, TaskState, task_state_manager
|
||||
from app.enums.task import OrderTaskStatus
|
||||
from app.repositories.task_repository import TaskRepository
|
||||
from app.schemas.task import (
|
||||
CardInfo,
|
||||
GiftCardSubmissionRequest,
|
||||
GiftCardSubmissionResponse,
|
||||
TaskCardInfo,
|
||||
TaskLinkInfo,
|
||||
GiftCardDetailCreate,
|
||||
LinkInfo,
|
||||
TaskListItem,
|
||||
TaskListResponse,
|
||||
TaskUserInfo,
|
||||
GiftCardDetailCreate,
|
||||
UserInfo,
|
||||
)
|
||||
from app.services.gift_card_service import GiftCardService
|
||||
|
||||
@@ -132,7 +132,7 @@ class TaskService:
|
||||
user_info = None
|
||||
if order.user_data:
|
||||
user_data = order.user_data
|
||||
user_info = TaskUserInfo(
|
||||
user_info = UserInfo(
|
||||
id=user_data.id,
|
||||
first_name=user_data.first_name,
|
||||
last_name=user_data.last_name,
|
||||
@@ -150,7 +150,8 @@ class TaskService:
|
||||
link_info = None
|
||||
if order.links:
|
||||
link = order.links
|
||||
link_info = TaskLinkInfo(
|
||||
link_info = LinkInfo(
|
||||
weight=link.weight,
|
||||
id=link.id,
|
||||
url=link.url,
|
||||
amount=link.amount,
|
||||
@@ -170,7 +171,7 @@ class TaskService:
|
||||
if gift_card_list:
|
||||
card_info = []
|
||||
for gift_card in gift_card_list:
|
||||
card_info_item = TaskCardInfo(
|
||||
card_info_item = CardInfo(
|
||||
id=gift_card.id,
|
||||
card_code=gift_card.card_code,
|
||||
card_value=gift_card.card_value,
|
||||
|
||||
@@ -16,12 +16,14 @@ from app.repositories.repository_factory import RepositoryFactory
|
||||
from app.schemas.user_data import (
|
||||
UserDataBase,
|
||||
UserDataCreate,
|
||||
UserDataListResponse,
|
||||
UserDataResponse,
|
||||
UserDataStatsResponse,
|
||||
UserDataUpdate,
|
||||
UserDataUploadResponse,
|
||||
UserInfoResponse,
|
||||
)
|
||||
from app.schemas.task import (
|
||||
PaginatedResponse,
|
||||
UserInfo,
|
||||
)
|
||||
|
||||
|
||||
@@ -93,7 +95,7 @@ class UserDataService:
|
||||
return None
|
||||
return self._convert_to_response(user)
|
||||
|
||||
async def get_user_info(self, user_id: str) -> UserInfoResponse | None:
|
||||
async def get_user_info(self, user_id: str) -> UserInfo | None:
|
||||
"""
|
||||
获取用户完整信息(包含所有数据库字段)
|
||||
|
||||
@@ -177,7 +179,7 @@ class UserDataService:
|
||||
state: str | None = None,
|
||||
country: str | None = None,
|
||||
name_pattern: str | None = None,
|
||||
) -> UserDataListResponse:
|
||||
) -> PaginatedResponse[UserDataResponse]:
|
||||
"""
|
||||
获取用户数据列表
|
||||
|
||||
@@ -213,7 +215,7 @@ class UserDataService:
|
||||
total = result.total
|
||||
pages = result.pages
|
||||
|
||||
return UserDataListResponse(
|
||||
return PaginatedResponse[UserDataResponse](
|
||||
items=[self._convert_to_response(user) for user in users],
|
||||
total=total,
|
||||
page=page,
|
||||
@@ -228,7 +230,7 @@ class UserDataService:
|
||||
page_users = users[start_idx:end_idx]
|
||||
pages = (total + size - 1) // size
|
||||
|
||||
return UserDataListResponse(
|
||||
return PaginatedResponse[UserDataResponse](
|
||||
items=[self._convert_to_response(user) for user in page_users],
|
||||
total=total,
|
||||
page=page,
|
||||
@@ -282,7 +284,7 @@ class UserDataService:
|
||||
updated_at=user.updated_at.isoformat(),
|
||||
)
|
||||
|
||||
def _convert_to_info_response(self, user: UserData) -> UserInfoResponse:
|
||||
def _convert_to_info_response(self, user: UserData) -> UserInfo:
|
||||
"""
|
||||
将用户数据模型转换为完整信息响应模型
|
||||
|
||||
@@ -292,7 +294,7 @@ class UserDataService:
|
||||
Returns:
|
||||
用户完整信息响应模型
|
||||
"""
|
||||
return UserInfoResponse(
|
||||
return UserInfo(
|
||||
id=user.id,
|
||||
first_name=user.first_name,
|
||||
last_name=user.last_name,
|
||||
@@ -307,3 +309,15 @@ class UserDataService:
|
||||
full_name=user.full_name,
|
||||
full_address=user.full_address,
|
||||
)
|
||||
|
||||
async def delete_all_user_data(self, skip_orders: bool = False) -> dict[str, Any]:
|
||||
"""
|
||||
软删除所有用户数据
|
||||
|
||||
Args:
|
||||
skip_orders: 是否跳过有关联订单的用户数据
|
||||
|
||||
Returns:
|
||||
删除统计信息
|
||||
"""
|
||||
return await self.repo_factory.user_data.bulk_soft_delete(skip_orders=skip_orders)
|
||||
|
||||
@@ -70,6 +70,43 @@ async def _process_apple_order_async(
|
||||
"order_id": order_id,
|
||||
}
|
||||
|
||||
# 检查关联的用户数据是否已被软删除
|
||||
async with db_manager.get_async_session() as session:
|
||||
order_repo = OrderRepository(session)
|
||||
order = await order_repo.get_by_id(order_id, relations=["user_data"])
|
||||
|
||||
if not order:
|
||||
logger.error(f"订单不存在: {order_id}")
|
||||
await task_state_manager.fail_task(
|
||||
task_id, order_id, f"订单 {order_id} 不存在"
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"订单 {order_id} 不存在",
|
||||
"order_id": order_id,
|
||||
}
|
||||
|
||||
# 检查用户数据是否已被软删除
|
||||
if order.user_data.is_deleted:
|
||||
logger.warning(f"用户数据已被软删除,终止订单处理: {order_id}, user_data_id={order.user_data_id}")
|
||||
await task_state_manager.fail_task(
|
||||
task_id, order_id, f"用户数据 {order.user_data_id} 已被删除"
|
||||
)
|
||||
|
||||
# 更新订单状态为失败
|
||||
await order_repo.update_by_id(
|
||||
order_id,
|
||||
status=OrderStatus.FAILURE,
|
||||
failure_reason=f"用户数据 {order.user_data_id} 已被删除",
|
||||
completed_at=datetime.now(),
|
||||
)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"用户数据 {order.user_data_id} 已被删除",
|
||||
"order_id": order_id,
|
||||
}
|
||||
|
||||
# 获取分布式锁
|
||||
lock_key = f"apple_order_processing:{order_id}"
|
||||
lock = get_lock(
|
||||
|
||||
20
frontend/.hintrc
Normal file
20
frontend/.hintrc
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": [
|
||||
"development"
|
||||
],
|
||||
"hints": {
|
||||
"axe/forms": [
|
||||
"default",
|
||||
{
|
||||
"label": "off"
|
||||
}
|
||||
],
|
||||
"axe/name-role-value": [
|
||||
"default",
|
||||
{
|
||||
"button-name": "off"
|
||||
}
|
||||
],
|
||||
"button-type": "off"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Trash2, ExternalLink, Calendar, DollarSign, Copy, Loader2, Pause, Play } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Trash2, ExternalLink, Calendar, DollarSign, Copy, Loader2, Pause, Play, Edit3, Check, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
// 导入 Tooltip 组件
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/animate-ui/base/tooltip";
|
||||
@@ -11,11 +13,15 @@ interface LinkItemProps {
|
||||
link: LinkInfo;
|
||||
onDelete: (linkId: string) => void;
|
||||
onToggleStatus?: (linkId: string) => void;
|
||||
onUpdateWeight?: (linkId: string, weight: number) => void;
|
||||
isDeleting?: boolean;
|
||||
isTogglingStatus?: boolean;
|
||||
isUpdatingWeight?: boolean;
|
||||
}
|
||||
|
||||
export function LinkItem({ link, onDelete, onToggleStatus, isDeleting = false, isTogglingStatus = false }: LinkItemProps) {
|
||||
export function LinkItem({ link, onDelete, onToggleStatus, onUpdateWeight, isDeleting = false, isTogglingStatus = false, isUpdatingWeight = false }: LinkItemProps) {
|
||||
const [isEditingWeight, setIsEditingWeight] = useState(false);
|
||||
const [tempWeight, setTempWeight] = useState(link.weight);
|
||||
// 截断URL显示
|
||||
const truncateUrl = (url: string, maxLength: number = 30) => {
|
||||
if (url.length <= maxLength) return url;
|
||||
@@ -92,6 +98,31 @@ export function LinkItem({ link, onDelete, onToggleStatus, isDeleting = false, i
|
||||
}
|
||||
};
|
||||
|
||||
// 处理权重编辑
|
||||
const handleWeightEdit = () => {
|
||||
setTempWeight(link.weight);
|
||||
setIsEditingWeight(true);
|
||||
};
|
||||
|
||||
// 保存权重
|
||||
const handleSaveWeight = async () => {
|
||||
if (onUpdateWeight) {
|
||||
try {
|
||||
await onUpdateWeight(link.id, tempWeight);
|
||||
setIsEditingWeight(false);
|
||||
toast.success("权重更新成功");
|
||||
} catch {
|
||||
toast.error("权重更新失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 取消权重编辑
|
||||
const handleCancelWeightEdit = () => {
|
||||
setTempWeight(link.weight);
|
||||
setIsEditingWeight(false);
|
||||
};
|
||||
|
||||
const statusInfo = getStatusInfo(link.status);
|
||||
const StatusIcon = statusInfo.icon;
|
||||
|
||||
@@ -130,6 +161,54 @@ export function LinkItem({ link, onDelete, onToggleStatus, isDeleting = false, i
|
||||
{/* 第二行:金额、创建时间和操作按钮 */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* 权重信息 */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">权重:</span>
|
||||
{isEditingWeight ? (
|
||||
<div className="flex items-center space-x-1">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value={tempWeight}
|
||||
onChange={(e) => setTempWeight(Number(e.target.value))}
|
||||
className="w-12 px-1 py-0.5 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
||||
disabled={isUpdatingWeight}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSaveWeight}
|
||||
disabled={isUpdatingWeight}
|
||||
className="p-0.5 text-green-600 hover:bg-green-50 dark:hover:bg-green-900/20 rounded transition-colors"
|
||||
>
|
||||
{isUpdatingWeight ? <Loader2 className="h-3 w-3 animate-spin" /> : <Check className="h-3 w-3" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelWeightEdit}
|
||||
disabled={isUpdatingWeight}
|
||||
className="p-0.5 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm font-medium text-blue-600 dark:text-blue-400">
|
||||
{link.weight}
|
||||
</span>
|
||||
{onUpdateWeight && (
|
||||
<button
|
||||
onClick={handleWeightEdit}
|
||||
disabled={isUpdatingWeight}
|
||||
className="p-0.5 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
|
||||
>
|
||||
<Edit3 className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 金额信息 */}
|
||||
<div className="flex items-center space-x-1 text-sm">
|
||||
<DollarSign className="h-4 w-4 text-green-600" />
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Plus, RefreshCw, AlertCircle, Loader2, Trash2, Activity } from "lucide-react";
|
||||
import { Plus, RefreshCw, AlertCircle, Loader2, Trash2, Activity, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import {
|
||||
useCreateLinkApiV1LinksPost,
|
||||
useDeleteLinkApiV1LinksLinkIdDelete,
|
||||
useToggleLinkStatusApiV1LinksLinkIdStatusPatch,
|
||||
useUpdateLinkWeightApiV1LinksLinkIdWeightPatch,
|
||||
useGetLinksApiV1LinksListGet
|
||||
} from "@/lib/api/generated/link-management.gen";
|
||||
import { AppleButton } from "@/components/ui/apple-button";
|
||||
@@ -25,6 +26,8 @@ export function LinkManagement({
|
||||
refreshEnabled = false,
|
||||
refreshInterval = 5000,
|
||||
}: LinkManagementProps) {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const ITEMS_PER_PAGE = 5;
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [newUrl, setNewUrl] = useState("");
|
||||
const [newAmount, setNewAmount] = useState("");
|
||||
@@ -34,11 +37,15 @@ export function LinkManagement({
|
||||
const [isDeletingLink, setIsDeletingLink] = useState(false);
|
||||
const [isTogglingStatus, setIsTogglingStatus] = useState(false);
|
||||
const [togglingLinkId, setTogglingLinkId] = useState<string | null>(null);
|
||||
const [isUpdatingWeight, setIsUpdatingWeight] = useState(false);
|
||||
const [updatingWeightLinkId, setUpdatingWeightLinkId] = useState<string | null>(null);
|
||||
const [newWeight, setNewWeight] = useState("1");
|
||||
|
||||
// API hooks
|
||||
const createLinkMutation = useCreateLinkApiV1LinksPost();
|
||||
const deleteLinkMutation = useDeleteLinkApiV1LinksLinkIdDelete();
|
||||
const toggleLinkStatusMutation = useToggleLinkStatusApiV1LinksLinkIdStatusPatch();
|
||||
const updateLinkWeightMutation = useUpdateLinkWeightApiV1LinksLinkIdWeightPatch();
|
||||
|
||||
// 获取链接列表
|
||||
const {
|
||||
@@ -48,7 +55,7 @@ export function LinkManagement({
|
||||
refetch,
|
||||
isRefetching
|
||||
} = useGetLinksApiV1LinksListGet(
|
||||
{ page: 1, size: 50 },
|
||||
{ page: currentPage, size: ITEMS_PER_PAGE },
|
||||
{
|
||||
query: {
|
||||
enabled: true,
|
||||
@@ -71,12 +78,19 @@ export function LinkManagement({
|
||||
return;
|
||||
}
|
||||
|
||||
const weight = parseInt(newWeight) || 1;
|
||||
if (weight < 0 || weight > 100) {
|
||||
toast.error("权重必须在0-100之间");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAddingLink(true);
|
||||
try {
|
||||
await createLinkMutation.mutateAsync({
|
||||
data: {
|
||||
url: newUrl.trim(),
|
||||
amount: amount
|
||||
amount: amount,
|
||||
weight: weight
|
||||
}
|
||||
});
|
||||
toast.success("链接添加成功", {
|
||||
@@ -86,6 +100,7 @@ export function LinkManagement({
|
||||
setIsDialogOpen(false);
|
||||
setNewUrl("");
|
||||
setNewAmount("");
|
||||
setNewWeight("1");
|
||||
refetch();
|
||||
} catch (error) {
|
||||
toast.error("添加链接失败", {
|
||||
@@ -136,7 +151,7 @@ export function LinkManagement({
|
||||
const currentLink = linksData?.items?.find(link => link.id === linkId);
|
||||
const currentStatus = currentLink?.status || 'inactive';
|
||||
const newStatus = currentStatus === 'active' ? 'inactive' : 'active';
|
||||
|
||||
|
||||
await toggleLinkStatusMutation.mutateAsync({
|
||||
linkId: linkId,
|
||||
params: {
|
||||
@@ -153,8 +168,28 @@ export function LinkManagement({
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateWeight = async (linkId: string, weight: number) => {
|
||||
setUpdatingWeightLinkId(linkId);
|
||||
setIsUpdatingWeight(true);
|
||||
try {
|
||||
await updateLinkWeightMutation.mutateAsync({
|
||||
linkId: linkId,
|
||||
params: {
|
||||
weight: weight
|
||||
}
|
||||
});
|
||||
refetch();
|
||||
} catch (error) {
|
||||
throw error; // 重新抛出错误,由 LinkItem 组件处理
|
||||
} finally {
|
||||
setIsUpdatingWeight(false);
|
||||
setUpdatingWeightLinkId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const links = linksData?.items || [];
|
||||
const total = linksData?.total || 0;
|
||||
const totalPages = Math.ceil(total / ITEMS_PER_PAGE);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -240,6 +275,26 @@ export function LinkManagement({
|
||||
金额支持小数,最多保留两位小数
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="weight" className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
权重 *
|
||||
</Label>
|
||||
<Input
|
||||
id="weight"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value={newWeight}
|
||||
onChange={(e) => setNewWeight(e.target.value)}
|
||||
className="apple-input"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
权重范围0-100,用于控制链接的优先级
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex gap-2 pt-4">
|
||||
@@ -252,7 +307,7 @@ export function LinkManagement({
|
||||
</AppleButton>
|
||||
<AppleButton
|
||||
onClick={handleAddLink}
|
||||
disabled={isAddingLink || !newUrl.trim() || !newAmount}
|
||||
disabled={isAddingLink || !newUrl.trim() || !newAmount || !newWeight}
|
||||
className="flex items-center gap-2 apple-glass-button"
|
||||
>
|
||||
{isAddingLink ? (
|
||||
@@ -356,8 +411,10 @@ export function LinkManagement({
|
||||
link={link}
|
||||
onDelete={handleDeleteLink}
|
||||
onToggleStatus={handleToggleStatus}
|
||||
onUpdateWeight={handleUpdateWeight}
|
||||
isDeleting={isDeletingLink}
|
||||
isTogglingStatus={isTogglingStatus && togglingLinkId === link.id}
|
||||
isUpdatingWeight={isUpdatingWeight && updatingWeightLinkId === link.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -365,11 +422,74 @@ export function LinkManagement({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分页控件 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between pt-4 mt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
第 {currentPage} 页,共 {totalPages} 页
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-500">
|
||||
(总计 {total} 个链接)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<AppleButton
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="w-8 h-8 rounded-lg"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</AppleButton>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
let pageNum;
|
||||
if (totalPages <= 5) {
|
||||
pageNum = i + 1;
|
||||
} else if (currentPage <= 3) {
|
||||
pageNum = i + 1;
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
pageNum = totalPages - 4 + i;
|
||||
} else {
|
||||
pageNum = currentPage - 2 + i;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppleButton
|
||||
key={pageNum}
|
||||
variant={currentPage === pageNum ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(pageNum)}
|
||||
className={`w-8 h-8 rounded-lg ${currentPage === pageNum ? 'apple-glass-button' : ''}`}
|
||||
>
|
||||
{pageNum}
|
||||
</AppleButton>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<AppleButton
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="w-8 h-8 rounded-lg"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</AppleButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部信息 */}
|
||||
{total > 0 && (
|
||||
<div className="flex justify-between items-center pt-4 mt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
显示 {links.length} / {total} 个链接
|
||||
当前页显示 {links.length} 个链接
|
||||
</span>
|
||||
{refreshEnabled && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-500">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import {
|
||||
RefreshCw,
|
||||
User,
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
Loader2,
|
||||
Gift
|
||||
Gift,
|
||||
ChevronLeft,
|
||||
ChevronRight
|
||||
} from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/animate-ui/base/tooltip";
|
||||
import { useGetTaskListApiV1TasksListGet } from "@/lib/api/generated/task-management.gen";
|
||||
@@ -30,6 +32,8 @@ interface TaskListProps {
|
||||
|
||||
export function TaskList({ refreshEnabled = false, refreshInterval = 5000, className }: TaskListProps) {
|
||||
const [isLocalRefreshing, setIsLocalRefreshing] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const ITEMS_PER_PAGE = 12;
|
||||
|
||||
// 获取任务列表
|
||||
const {
|
||||
@@ -56,6 +60,25 @@ export function TaskList({ refreshEnabled = false, refreshInterval = 5000, class
|
||||
|
||||
const tasks = taskListData?.tasks || [];
|
||||
|
||||
// 排序:waiting_gift_card 状态的任务优先
|
||||
const sortedTasks = useMemo(() => {
|
||||
return [...tasks].sort((a, b) => {
|
||||
if (a.status === "waiting_gift_card" && b.status !== "waiting_gift_card") {
|
||||
return -1; // a 排在前面
|
||||
}
|
||||
if (a.status !== "waiting_gift_card" && b.status === "waiting_gift_card") {
|
||||
return 1; // b 排在前面
|
||||
}
|
||||
return 0; // 保持原有顺序
|
||||
});
|
||||
}, [tasks]);
|
||||
|
||||
// 分页逻辑
|
||||
const totalPages = Math.ceil(sortedTasks.length / ITEMS_PER_PAGE);
|
||||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||
const endIndex = startIndex + ITEMS_PER_PAGE;
|
||||
const paginatedTasks = sortedTasks.slice(startIndex, endIndex);
|
||||
|
||||
// 截断错误信息
|
||||
const truncateErrorMessage = (message: string, maxLength: number = 50) => {
|
||||
if (message.length <= maxLength) return message;
|
||||
@@ -179,163 +202,228 @@ export function TaskList({ refreshEnabled = false, refreshInterval = 5000, class
|
||||
)}
|
||||
|
||||
{!isLoading && !error && tasks.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{tasks.map((task) => (
|
||||
<div
|
||||
key={task.task_id}
|
||||
className="bg-white dark:bg-gray-800 rounded-xl p-4 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
{/* 任务头部 */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon(task.status)}
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
任务 {task.task_id.slice(-8)}
|
||||
</span>
|
||||
</div>
|
||||
<Badge className={cn("px-2 py-1 rounded-full text-xs font-medium", getStatusColor(task.status))}>
|
||||
{getStatusText(task.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatTime(task.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 用户信息 */}
|
||||
{task.user_info && (
|
||||
<div className="space-y-2 mb-3 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-4 w-4 text-gray-500" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{task.user_info.first_name} {task.user_info.last_name}
|
||||
</span>
|
||||
</div>
|
||||
{/* 用户邮箱 */}
|
||||
{task.user_info.email && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{paginatedTasks.map((task) => (
|
||||
<div
|
||||
key={task.task_id}
|
||||
className="bg-white dark:bg-gray-800 rounded-xl p-4 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
{/* 任务头部 */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{task.user_info.email}
|
||||
{getStatusIcon(task.status)}
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
任务 {task.task_id.slice(-8)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* 用户ID */}
|
||||
{task.user_info?.first_name && (
|
||||
<Badge className={cn("px-2 py-1 rounded-full text-xs font-medium", getStatusColor(task.status))}>
|
||||
{getStatusText(task.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatTime(task.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 用户信息 */}
|
||||
{task.user_info && (
|
||||
<div className="space-y-2 mb-3 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-gray-400"></div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-500">
|
||||
用户昵称: {task.user_info?.first_name} {task.user_info?.last_name}
|
||||
<User className="h-4 w-4 text-gray-500" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{task.user_info.first_name} {task.user_info.last_name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 用户邮箱 */}
|
||||
{task.user_info.email && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{task.user_info.email}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* 用户ID */}
|
||||
{task.user_info?.first_name && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-gray-400"></div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-500">
|
||||
用户昵称: {task.user_info?.first_name} {task.user_info?.last_name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 链接信息 */}
|
||||
{task.link_info && (
|
||||
<div className="space-y-2 mb-3">
|
||||
{/* 链接信息 */}
|
||||
{task.link_info && (
|
||||
<div className="space-y-2 mb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm font-medium text-green-600">
|
||||
${task.link_info.amount || 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 链接 URL */}
|
||||
{task.link_info.url && (
|
||||
<div className="flex items-start gap-2">
|
||||
<Link className="h-3 w-3 text-gray-400 mt-0.5" />
|
||||
<a
|
||||
href={task.link_info.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline break-all"
|
||||
>
|
||||
{task.link_info.url}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 进度条 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm font-medium text-green-600">
|
||||
${task.link_info.amount || 0}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
任务进度
|
||||
</span>
|
||||
<span className="text-xs font-medium text-gray-900 dark:text-gray-100">
|
||||
<AnimatedNumber
|
||||
value={task.progress}
|
||||
duration={500}
|
||||
decimals={1}
|
||||
/>%
|
||||
</span>
|
||||
</div>
|
||||
{/* 链接 URL */}
|
||||
{task.link_info.url && (
|
||||
<div className="flex items-start gap-2">
|
||||
<Link className="h-3 w-3 text-gray-400 mt-0.5" />
|
||||
<a
|
||||
href={task.link_info.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline break-all"
|
||||
>
|
||||
{task.link_info.url}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 进度条 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
任务进度
|
||||
</span>
|
||||
<span className="text-xs font-medium text-gray-900 dark:text-gray-100">
|
||||
<AnimatedNumber
|
||||
value={task.progress}
|
||||
duration={500}
|
||||
decimals={1}
|
||||
/>%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={task.progress}
|
||||
className={cn(
|
||||
"h-2",
|
||||
task.progress === 100
|
||||
? "[&>[data-slot='progress-indicator']]:bg-green-600"
|
||||
: task.progress > 0 ? "[&>[data-slot='progress-indicator']]:bg-blue-600" : "[&>[data-slot='progress-indicator']]:bg-gray-400"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 礼品卡输入框 - 只在等待礼品卡状态显示 */}
|
||||
{task.status === "waiting_gift_card" && task.link_info?.amount && (
|
||||
<div className="mt-4">
|
||||
<GiftCardInput
|
||||
taskId={task.task_id}
|
||||
amount={task.link_info.amount}
|
||||
updatedAt={task.updated_at}
|
||||
onSubmit={(success) => {
|
||||
if (success) {
|
||||
refetch();
|
||||
}
|
||||
}}
|
||||
triggerButton={
|
||||
<AppleButton variant="default" size="sm" className="w-full flex items-center gap-2 apple-glass-button">
|
||||
<Gift className="h-4 w-4" />
|
||||
提交礼品卡 (${task.link_info.amount})
|
||||
</AppleButton>
|
||||
}
|
||||
<Progress
|
||||
value={task.progress}
|
||||
className={cn(
|
||||
"h-2",
|
||||
task.progress === 100
|
||||
? "[&>[data-slot='progress-indicator']]:bg-green-600"
|
||||
: task.progress > 0 ? "[&>[data-slot='progress-indicator']]:bg-blue-600" : "[&>[data-slot='progress-indicator']]:bg-gray-400"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误信息 */}
|
||||
{task.error_message && (
|
||||
<div className="mt-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-red-200 dark:border-red-800 shadow-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
{task.error_message.length > 50 ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className="text-sm text-red-700 dark:text-red-300 cursor-help font-medium break-words">
|
||||
{truncateErrorMessage(task.error_message)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 shadow-lg">
|
||||
<p className="max-w-xs text-sm font-medium leading-relaxed break-words">{task.error_message}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className="text-sm text-red-700 dark:text-red-300 font-medium break-words">
|
||||
{task.error_message}
|
||||
</span>
|
||||
)}
|
||||
{/* 礼品卡输入框 - 只在等待礼品卡状态显示 */}
|
||||
{task.status === "waiting_gift_card" && task.link_info?.amount && (
|
||||
<div className="mt-4">
|
||||
<GiftCardInput
|
||||
taskId={task.task_id}
|
||||
amount={task.link_info.amount}
|
||||
updatedAt={task.updated_at}
|
||||
onSubmit={(success) => {
|
||||
if (success) {
|
||||
refetch();
|
||||
}
|
||||
}}
|
||||
triggerButton={
|
||||
<AppleButton variant="default" size="sm" className="w-full flex items-center gap-2 apple-glass-button">
|
||||
<Gift className="h-4 w-4" />
|
||||
提交礼品卡 (${task.link_info.amount})
|
||||
</AppleButton>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误信息 */}
|
||||
{task.error_message && (
|
||||
<div className="mt-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-red-200 dark:border-red-800 shadow-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
{task.error_message.length > 50 ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className="text-sm text-red-700 dark:text-red-300 cursor-help font-medium break-words">
|
||||
{truncateErrorMessage(task.error_message)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 shadow-lg">
|
||||
<p className="max-w-xs text-sm font-medium leading-relaxed break-words">{task.error_message}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className="text-sm text-red-700 dark:text-red-300 font-medium break-words">
|
||||
{task.error_message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 分页控件 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between pt-4 mt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
第 {currentPage} 页,共 {totalPages} 页
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-500">
|
||||
({sortedTasks.length} 个任务)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<AppleButton
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="w-8 h-8 rounded-lg"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</AppleButton>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
let pageNum;
|
||||
if (totalPages <= 5) {
|
||||
pageNum = i + 1;
|
||||
} else if (currentPage <= 3) {
|
||||
pageNum = i + 1;
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
pageNum = totalPages - 4 + i;
|
||||
} else {
|
||||
pageNum = currentPage - 2 + i;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppleButton
|
||||
key={pageNum}
|
||||
variant={currentPage === pageNum ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(pageNum)}
|
||||
className={`w-8 h-8 rounded-lg ${currentPage === pageNum ? 'apple-glass-button' : ''}`}
|
||||
>
|
||||
{pageNum}
|
||||
</AppleButton>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AppleButton
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="w-8 h-8 rounded-lg"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</AppleButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -343,7 +431,7 @@ export function TaskList({ refreshEnabled = false, refreshInterval = 5000, class
|
||||
{!isLoading && !error && (
|
||||
<div className="flex justify-between items-center pt-4 mt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
显示 {tasks.length} 个任务
|
||||
显示 {paginatedTasks.length} 个任务 (共 {tasks.length} 个)
|
||||
</span>
|
||||
{refreshEnabled && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-500">
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { AppleButton } from "@/components/ui/apple-button";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { Plus, Database } from "lucide-react";
|
||||
import { Plus, Database, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { BrushCleaning } from "../animate-ui/icons/brush-cleaning";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/animate-ui/base/tooltip';
|
||||
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { useInterval } from "@/lib/hooks/use-timeout";
|
||||
import { useGetUserDataListApiV1UserDataListGet } from "@/lib/api/generated/user-data-management.gen";
|
||||
import { useGetUserDataListApiV1UserDataListGet, useBulkDeleteAllUserDataApiV1UserDataAllDelete } from "@/lib/api/generated/user-data-management.gen";
|
||||
import { FileUploadModal } from "@/components/forms/file-upload-modal";
|
||||
import { GetUserDataListApiV1UserDataListGetParams } from "@/lib/api/generated/schemas";
|
||||
|
||||
@@ -37,11 +38,14 @@ const userDataService = {
|
||||
|
||||
export function UploadedDataDisplay({ refreshEnabled = true, refreshInterval = 5000 }: UploadedDataDisplayProps) {
|
||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const { data: userDataList, isLoading, refetch } = userDataService.useUserDataList({
|
||||
page: 1,
|
||||
size: 10
|
||||
});
|
||||
|
||||
const bulkDeleteMutation = useBulkDeleteAllUserDataApiV1UserDataAllDelete();
|
||||
|
||||
// 获取数据
|
||||
const fetchUploadedData = async () => {
|
||||
try {
|
||||
@@ -73,16 +77,20 @@ export function UploadedDataDisplay({ refreshEnabled = true, refreshInterval = 5
|
||||
setIsUploadModalOpen(false);
|
||||
};
|
||||
|
||||
// 清空数据
|
||||
const handleClearData = async () => {
|
||||
// 处理删除所有数据
|
||||
const handleDeleteAllData = async () => {
|
||||
try {
|
||||
console.log("数据清空功能待实现");
|
||||
await fetchUploadedData();
|
||||
} catch {
|
||||
console.error("清空数据失败");
|
||||
await bulkDeleteMutation.mutateAsync({});
|
||||
await refetch();
|
||||
setIsDeleteDialogOpen(false);
|
||||
toast.success("所有用户数据已成功删除");
|
||||
} catch (error) {
|
||||
console.error("删除所有数据失败:", error);
|
||||
toast.error("删除数据失败,请稍后重试");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="apple-glass-card rounded-2xl p-6 transition-all duration-300">
|
||||
@@ -106,6 +114,47 @@ export function UploadedDataDisplay({ refreshEnabled = true, refreshInterval = 5
|
||||
{userDataList?.total}
|
||||
</Badge>
|
||||
)}
|
||||
{(userDataList?.total || 0) > 0 && (
|
||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<AppleButton variant="destructive" size="sm" className="flex items-center gap-2">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
删除所有
|
||||
</AppleButton>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="apple-glass-card max-w-md rounded-2xl shadow-2xl border-0 bg-white/90 dark:bg-gray-900/90 backdrop-blur-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-red-600">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
确认删除所有数据
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
您确定要删除所有已上传的用户数据吗?此操作无法撤销。
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||
当前共有 {userDataList?.total || 0} 条数据将被删除。
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter className="gap-2">
|
||||
<AppleButton
|
||||
variant="outline"
|
||||
onClick={() => setIsDeleteDialogOpen(false)}
|
||||
>
|
||||
取消
|
||||
</AppleButton>
|
||||
<AppleButton
|
||||
variant="destructive"
|
||||
onClick={handleDeleteAllData}
|
||||
disabled={bulkDeleteMutation.isPending}
|
||||
>
|
||||
{bulkDeleteMutation.isPending ? "删除中..." : "确认删除"}
|
||||
</AppleButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
<Dialog open={isUploadModalOpen} onOpenChange={setIsUploadModalOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<AppleButton variant="default" size="sm" className="flex items-center gap-2 apple-glass-button">
|
||||
@@ -124,27 +173,99 @@ export function UploadedDataDisplay({ refreshEnabled = true, refreshInterval = 5
|
||||
</div>
|
||||
</div>
|
||||
) : userDataList?.items.length ? (
|
||||
<ScrollArea className="h-[150px] max-h-[200px] w-full rounded-md">
|
||||
<div className="p-4 space-y-2">
|
||||
<ScrollArea className="h-[300px] w-full rounded-lg">
|
||||
<div className="p-4 space-y-4">
|
||||
{userDataList.items.map((item) => (
|
||||
<TooltipProvider key={item.id}>
|
||||
<Tooltip hoverable>
|
||||
<TooltipTrigger render={<div
|
||||
key={item.id}
|
||||
className="flex justify-between items-center p-2 rounded-md bg-card hover:bg-secondary/50 group"
|
||||
>
|
||||
<div className="text-sm font-mono truncate max-w-[100%]">
|
||||
{item.first_name} {item.last_name} {item.street_address} {item.city} {item.state} {item.zip_code} {item.email} {item.phone}
|
||||
<TooltipTrigger render={
|
||||
<div className="bg-white dark:bg-gray-800 p-3 rounded-xl border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all duration-300 hover:scale-[1.01] shadow-sm">
|
||||
{/* 用户头像和基本信息 */}
|
||||
<div className="flex items-start gap-2 mb-3">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm shadow-md flex-shrink-0">
|
||||
{item.first_name?.[0] || 'U'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-gray-100 text-sm leading-tight">
|
||||
{item.first_name} {item.last_name}
|
||||
</h4>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-medium">
|
||||
ID: {item.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 详细信息网格 - 紧凑布局 */}
|
||||
<div className="space-y-2">
|
||||
{/* 地址信息 */}
|
||||
<div className="flex items-start gap-2 p-2 bg-gray-50 dark:bg-gray-700/30 rounded-md">
|
||||
<div className="w-6 h-6 rounded-full bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-xs">📍</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium text-gray-600 dark:text-gray-300 mb-0.5 uppercase tracking-wide">
|
||||
地址
|
||||
</div>
|
||||
<div className="text-xs text-gray-900 dark:text-gray-100 break-words leading-relaxed">
|
||||
{item.street_address}, {item.city}, {item.state} {item.zip_code}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 邮箱信息 */}
|
||||
<div className="flex items-start gap-2 p-2 bg-gray-50 dark:bg-gray-700/30 rounded-md">
|
||||
<div className="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-xs">✉️</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium text-gray-600 dark:text-gray-300 mb-0.5 uppercase tracking-wide">
|
||||
邮箱
|
||||
</div>
|
||||
<div className="text-xs text-gray-900 dark:text-gray-100 break-words leading-relaxed">
|
||||
{item.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 电话信息 */}
|
||||
<div className="flex items-start gap-2 p-2 bg-gray-50 dark:bg-gray-700/30 rounded-md">
|
||||
<div className="w-6 h-6 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-xs">📞</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium text-gray-600 dark:text-gray-300 mb-0.5 uppercase tracking-wide">
|
||||
电话
|
||||
</div>
|
||||
<div className="text-xs text-gray-900 dark:text-gray-100 break-words leading-relaxed">
|
||||
{item.phone}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} />
|
||||
<TooltipContent className="bg-gray-900 dark:bg-gray-800 text-white border-none max-w-md p-4">
|
||||
<div className="space-y-2">
|
||||
<div className="font-semibold text-lg">{item.first_name} {item.last_name}</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div>📍 {item.street_address}, {item.city}, {item.state} {item.zip_code}</div>
|
||||
<div>✉️ {item.email}</div>
|
||||
<div>📞 {item.phone}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>} />
|
||||
<TooltipContent className="bg-blue-600 dark:bg-blue-700 text-white border-none">
|
||||
<p>{item.first_name} {item.last_name} {item.street_address} {item.city} {item.state} {item.zip_code} {item.email} {item.phone}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
<ScrollBar
|
||||
orientation="vertical"
|
||||
className="w-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors rounded-full"
|
||||
/>
|
||||
<ScrollBar
|
||||
orientation="horizontal"
|
||||
className="h-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors rounded-full"
|
||||
/>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
@@ -152,17 +273,6 @@ export function UploadedDataDisplay({ refreshEnabled = true, refreshInterval = 5
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(userDataList?.total || 0) > 0 && (
|
||||
<div className="flex items-center pt-4 mt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<AppleButton
|
||||
variant="outline"
|
||||
className="w-full flex items-center gap-2 apple-glass-button"
|
||||
onClick={handleClearData}
|
||||
>
|
||||
<BrushCleaning animateOnTap />清空数据
|
||||
</AppleButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 文件上传模态框 */}
|
||||
|
||||
@@ -22,12 +22,14 @@ import type {
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import type {
|
||||
DeleteLinkApiV1LinksLinkIdDelete200,
|
||||
GetLinksApiV1LinksListGetParams,
|
||||
HTTPValidationError,
|
||||
LinkCreate,
|
||||
LinkInfo,
|
||||
PaginatedResponseLinkInfo,
|
||||
ToggleLinkStatusApiV1LinksLinkIdStatusPatchParams,
|
||||
UpdateLinkWeightApiV1LinksLinkIdWeightPatchParams,
|
||||
} from "./schemas";
|
||||
|
||||
import { axiosClient } from "../enhanced-axios-client";
|
||||
@@ -476,7 +478,7 @@ const deleteLinkApiV1LinksLinkIdDelete = (
|
||||
linkId: string,
|
||||
options?: SecondParameter<typeof axiosClient>,
|
||||
) => {
|
||||
return axiosClient<unknown>(
|
||||
return axiosClient<DeleteLinkApiV1LinksLinkIdDelete200>(
|
||||
{ url: `/api/v1/links/${linkId}`, method: "DELETE" },
|
||||
{ second: true, ...options },
|
||||
);
|
||||
@@ -563,7 +565,7 @@ const toggleLinkStatusApiV1LinksLinkIdStatusPatch = (
|
||||
params: ToggleLinkStatusApiV1LinksLinkIdStatusPatchParams,
|
||||
options?: SecondParameter<typeof axiosClient>,
|
||||
) => {
|
||||
return axiosClient<unknown>(
|
||||
return axiosClient<LinkInfo>(
|
||||
{ url: `/api/v1/links/${linkId}/status`, method: "PATCH", params },
|
||||
{ second: true, ...options },
|
||||
);
|
||||
@@ -656,3 +658,105 @@ export const useToggleLinkStatusApiV1LinksLinkIdStatusPatch = <
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
/**
|
||||
* 更新链接权重
|
||||
* @summary Update Link Weight
|
||||
*/
|
||||
const updateLinkWeightApiV1LinksLinkIdWeightPatch = (
|
||||
linkId: string,
|
||||
params: UpdateLinkWeightApiV1LinksLinkIdWeightPatchParams,
|
||||
options?: SecondParameter<typeof axiosClient>,
|
||||
) => {
|
||||
return axiosClient<LinkInfo>(
|
||||
{ url: `/api/v1/links/${linkId}/weight`, method: "PATCH", params },
|
||||
{ second: true, ...options },
|
||||
);
|
||||
};
|
||||
|
||||
export const getUpdateLinkWeightApiV1LinksLinkIdWeightPatchMutationOptions = <
|
||||
TError = HTTPValidationError,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateLinkWeightApiV1LinksLinkIdWeightPatch>>,
|
||||
TError,
|
||||
{
|
||||
linkId: string;
|
||||
params: UpdateLinkWeightApiV1LinksLinkIdWeightPatchParams;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof axiosClient>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateLinkWeightApiV1LinksLinkIdWeightPatch>>,
|
||||
TError,
|
||||
{ linkId: string; params: UpdateLinkWeightApiV1LinksLinkIdWeightPatchParams },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["updateLinkWeightApiV1LinksLinkIdWeightPatch"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateLinkWeightApiV1LinksLinkIdWeightPatch>>,
|
||||
{
|
||||
linkId: string;
|
||||
params: UpdateLinkWeightApiV1LinksLinkIdWeightPatchParams;
|
||||
}
|
||||
> = (props) => {
|
||||
const { linkId, params } = props ?? {};
|
||||
|
||||
return updateLinkWeightApiV1LinksLinkIdWeightPatch(
|
||||
linkId,
|
||||
params,
|
||||
requestOptions,
|
||||
);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateLinkWeightApiV1LinksLinkIdWeightPatchMutationResult =
|
||||
NonNullable<
|
||||
Awaited<ReturnType<typeof updateLinkWeightApiV1LinksLinkIdWeightPatch>>
|
||||
>;
|
||||
|
||||
export type UpdateLinkWeightApiV1LinksLinkIdWeightPatchMutationError =
|
||||
HTTPValidationError;
|
||||
|
||||
/**
|
||||
* @summary Update Link Weight
|
||||
*/
|
||||
export const useUpdateLinkWeightApiV1LinksLinkIdWeightPatch = <
|
||||
TError = HTTPValidationError,
|
||||
TContext = unknown,
|
||||
>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateLinkWeightApiV1LinksLinkIdWeightPatch>>,
|
||||
TError,
|
||||
{
|
||||
linkId: string;
|
||||
params: UpdateLinkWeightApiV1LinksLinkIdWeightPatchParams;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof axiosClient>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateLinkWeightApiV1LinksLinkIdWeightPatch>>,
|
||||
TError,
|
||||
{ linkId: string; params: UpdateLinkWeightApiV1LinksLinkIdWeightPatchParams },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions =
|
||||
getUpdateLinkWeightApiV1LinksLinkIdWeightPatchMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Generated by orval v7.11.2 🍺
|
||||
* Do not edit manually.
|
||||
* Apple Gift Card Exchange
|
||||
* Apple礼品卡兑换服务后端API - 基于FastAPI的现代异步微服务架构
|
||||
* OpenAPI spec version: 2.0.0
|
||||
*/
|
||||
|
||||
export type BulkDeleteAllUserDataApiV1UserDataAllDeleteParams = {
|
||||
/**
|
||||
* 是否跳过有关联订单的用户数据
|
||||
*/
|
||||
skip_orders?: boolean;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Generated by orval v7.11.2 🍺
|
||||
* Do not edit manually.
|
||||
* Apple Gift Card Exchange
|
||||
* Apple礼品卡兑换服务后端API - 基于FastAPI的现代异步微服务架构
|
||||
* OpenAPI spec version: 2.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* 批量删除用户数据响应模型
|
||||
*/
|
||||
export interface BulkDeleteUserDataResponse {
|
||||
/** 总用户数 */
|
||||
total_users: number;
|
||||
/** 已删除用户数 */
|
||||
deleted_users: number;
|
||||
/** 跳过的用户数 */
|
||||
skipped_users: number;
|
||||
/** 响应消息 */
|
||||
message: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Generated by orval v7.11.2 🍺
|
||||
* Do not edit manually.
|
||||
* Apple Gift Card Exchange
|
||||
* Apple礼品卡兑换服务后端API - 基于FastAPI的现代异步微服务架构
|
||||
* OpenAPI spec version: 2.0.0
|
||||
*/
|
||||
|
||||
export type DeleteLinkApiV1LinksLinkIdDelete200 = { [key: string]: string };
|
||||
@@ -6,10 +6,13 @@
|
||||
* OpenAPI spec version: 2.0.0
|
||||
*/
|
||||
|
||||
export * from "./bulkDeleteAllUserDataApiV1UserDataAllDeleteParams";
|
||||
export * from "./bulkDeleteUserDataResponse";
|
||||
export * from "./cardInfo";
|
||||
export * from "./cardInfoFailureReason";
|
||||
export * from "./deleteAllDataResponse";
|
||||
export * from "./deleteAllDataResponseDeletedTables";
|
||||
export * from "./deleteLinkApiV1LinksLinkIdDelete200";
|
||||
export * from "./exportOrdersApiV1OrdersExportGetParams";
|
||||
export * from "./getLinksApiV1LinksListGetParams";
|
||||
export * from "./getOrdersApiV1OrdersListGetParams";
|
||||
@@ -53,6 +56,7 @@ export * from "./taskListItemWorkerId";
|
||||
export * from "./taskListResponse";
|
||||
export * from "./taskStateResponse";
|
||||
export * from "./toggleLinkStatusApiV1LinksLinkIdStatusPatchParams";
|
||||
export * from "./updateLinkWeightApiV1LinksLinkIdWeightPatchParams";
|
||||
export * from "./userDataCreate";
|
||||
export * from "./userDataResponse";
|
||||
export * from "./userDataUploadResponse";
|
||||
|
||||
@@ -20,6 +20,12 @@ export interface LinkCreate {
|
||||
* 金额
|
||||
*/
|
||||
amount: number;
|
||||
/**
|
||||
* 权重(1-100)
|
||||
* @minimum 1
|
||||
* @maximum 100
|
||||
*/
|
||||
weight?: number;
|
||||
/** 链接状态 */
|
||||
status?: LinkStatus;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface LinkInfo {
|
||||
url: string;
|
||||
/** 金额 */
|
||||
amount: number;
|
||||
/** 权重(1-100) */
|
||||
weight: number;
|
||||
/** 链接状态 */
|
||||
status: LinkStatus;
|
||||
/** 创建时间 */
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Generated by orval v7.11.2 🍺
|
||||
* Do not edit manually.
|
||||
* Apple Gift Card Exchange
|
||||
* Apple礼品卡兑换服务后端API - 基于FastAPI的现代异步微服务架构
|
||||
* OpenAPI spec version: 2.0.0
|
||||
*/
|
||||
|
||||
export type UpdateLinkWeightApiV1LinksLinkIdWeightPatchParams = {
|
||||
/**
|
||||
* 权重值(1-100)
|
||||
* @minimum 1
|
||||
* @maximum 100
|
||||
*/
|
||||
weight: number;
|
||||
};
|
||||
@@ -22,6 +22,8 @@ import type {
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import type {
|
||||
BulkDeleteAllUserDataApiV1UserDataAllDeleteParams,
|
||||
BulkDeleteUserDataResponse,
|
||||
GetUserDataListApiV1UserDataListGetParams,
|
||||
HTTPValidationError,
|
||||
PaginatedResponseUserDataResponse,
|
||||
@@ -906,3 +908,95 @@ export const useBatchUploadUserDataApiV1UserDataBatchUploadPost = <
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
/**
|
||||
* 批量软删除所有用户数据
|
||||
|
||||
- **skip_orders**: 是否跳过有关联订单的用户数据(默认false,会删除包括有关联订单的所有数据)
|
||||
|
||||
返回删除统计信息,包括总用户数、删除用户数和跳过用户数
|
||||
* @summary 批量删除所有用户数据
|
||||
*/
|
||||
const bulkDeleteAllUserDataApiV1UserDataAllDelete = (
|
||||
params?: BulkDeleteAllUserDataApiV1UserDataAllDeleteParams,
|
||||
options?: SecondParameter<typeof axiosClient>,
|
||||
) => {
|
||||
return axiosClient<BulkDeleteUserDataResponse>(
|
||||
{ url: `/api/v1/user-data/all`, method: "DELETE", params },
|
||||
{ second: true, ...options },
|
||||
);
|
||||
};
|
||||
|
||||
export const getBulkDeleteAllUserDataApiV1UserDataAllDeleteMutationOptions = <
|
||||
TError = HTTPValidationError,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof bulkDeleteAllUserDataApiV1UserDataAllDelete>>,
|
||||
TError,
|
||||
{ params?: BulkDeleteAllUserDataApiV1UserDataAllDeleteParams },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof axiosClient>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof bulkDeleteAllUserDataApiV1UserDataAllDelete>>,
|
||||
TError,
|
||||
{ params?: BulkDeleteAllUserDataApiV1UserDataAllDeleteParams },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["bulkDeleteAllUserDataApiV1UserDataAllDelete"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof bulkDeleteAllUserDataApiV1UserDataAllDelete>>,
|
||||
{ params?: BulkDeleteAllUserDataApiV1UserDataAllDeleteParams }
|
||||
> = (props) => {
|
||||
const { params } = props ?? {};
|
||||
|
||||
return bulkDeleteAllUserDataApiV1UserDataAllDelete(params, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type BulkDeleteAllUserDataApiV1UserDataAllDeleteMutationResult =
|
||||
NonNullable<
|
||||
Awaited<ReturnType<typeof bulkDeleteAllUserDataApiV1UserDataAllDelete>>
|
||||
>;
|
||||
|
||||
export type BulkDeleteAllUserDataApiV1UserDataAllDeleteMutationError =
|
||||
HTTPValidationError;
|
||||
|
||||
/**
|
||||
* @summary 批量删除所有用户数据
|
||||
*/
|
||||
export const useBulkDeleteAllUserDataApiV1UserDataAllDelete = <
|
||||
TError = HTTPValidationError,
|
||||
TContext = unknown,
|
||||
>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof bulkDeleteAllUserDataApiV1UserDataAllDelete>>,
|
||||
TError,
|
||||
{ params?: BulkDeleteAllUserDataApiV1UserDataAllDeleteParams },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof axiosClient>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof bulkDeleteAllUserDataApiV1UserDataAllDelete>>,
|
||||
TError,
|
||||
{ params?: BulkDeleteAllUserDataApiV1UserDataAllDeleteParams },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions =
|
||||
getBulkDeleteAllUserDataApiV1UserDataAllDeleteMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user