feat(links): 实现基于权重的轮询算法和链接管理功能

- 新增链接权重字段,支持1-100范围设置
- 修改轮询算法为基于权重的选择机制
- 更新链接API接口返回统一使用LinkInfo模型
- 添加更新链接权重的PATCH端点
- 调整链接仓库查询逻辑,只包含激活状态链接
- 迁移链接相关Pydantic模型到task模块统一管理
- 修改分页响应格式为通用PaginatedResponse包装
- 禁用OpenTelemetry监控配置
This commit is contained in:
danial
2025-09-30 17:02:02 +08:00
parent 736a7467e6
commit 8bc8e1c664
32 changed files with 1426 additions and 539 deletions

View File

@@ -54,15 +54,15 @@ LOG_MAX_SIZE=10485760
LOG_BACKUP_COUNT=5 LOG_BACKUP_COUNT=5
# OpenTelemetry简化配置 # OpenTelemetry简化配置
OTEL_ENABLED=true OTEL_ENABLED=false
OTEL_SERVICE_NAME=apple-exchange-backend OTEL_SERVICE_NAME=苹果官网下单
OTEL_SERVICE_VERSION=2.0.0 OTEL_SERVICE_VERSION=2.0.0
OTEL_EXPORTER_ENDPOINT=http://38.38.251.113:31547 OTEL_EXPORTER_ENDPOINT=http://38.38.251.113:31547
OTEL_EXPORTER_PROTOCOL=grpc OTEL_EXPORTER_PROTOCOL=grpc
OTEL_EXPORTER_TIMEOUT=30 OTEL_EXPORTER_TIMEOUT=30
OTEL_TRACES_ENABLED=true OTEL_TRACES_ENABLED=false
OTEL_METRICS_ENABLED=true OTEL_METRICS_ENABLED=false
OTEL_LOGS_ENABLED=true OTEL_LOGS_ENABLED=false
OTEL_SAMPLER_RATIO=1.0 OTEL_SAMPLER_RATIO=1.0
OTEL_BATCH_SIZE=512 OTEL_BATCH_SIZE=512
OTEL_EXPORT_INTERVAL=5000 OTEL_EXPORT_INTERVAL=5000

View File

@@ -9,9 +9,12 @@ from app.core.database import get_async_db
from app.core.log import get_logger from app.core.log import get_logger
from app.schemas.link import ( from app.schemas.link import (
LinkCreate, LinkCreate,
LinkListResponse,
LinkResponse,
LinkStatus, LinkStatus,
LinkUpdate,
)
from app.schemas.task import (
LinkInfo,
PaginatedResponse,
) )
from app.services.link_service import LinksService 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) return LinksService(db)
@router.post("/", response_model=LinkResponse) @router.post("/", response_model=LinkInfo)
async def create_link( async def create_link(
link_data: LinkCreate, link_service: LinksService = Depends(get_link_service) link_data: LinkCreate, link_service: LinksService = Depends(get_link_service)
): ) -> LinkInfo:
"""创建新链接""" """创建新链接"""
try: try:
return await link_service.create_link(link_data) return await link_service.create_link(link_data)
@@ -41,7 +44,7 @@ async def create_link(
raise HTTPException(status_code=500, detail="创建链接失败") raise HTTPException(status_code=500, detail="创建链接失败")
@router.get("/list", response_model=LinkListResponse) @router.get("/list", response_model=PaginatedResponse[LinkInfo])
async def get_links( async def get_links(
page: int = Query(1, ge=1, description="页码"), page: int = Query(1, ge=1, description="页码"),
size: int = Query(20, ge=1, le=1000, 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="最大金额"), max_amount: float | None = Query(None, description="最大金额"),
url_pattern: str | None = Query(None, description="URL模式"), url_pattern: str | None = Query(None, description="URL模式"),
link_service: LinksService = Depends(get_link_service), link_service: LinksService = Depends(get_link_service),
): ) -> PaginatedResponse[LinkInfo]:
"""获取链接列表""" """获取链接列表"""
try: try:
result = await link_service.get_links( result = await link_service.get_links(
@@ -66,10 +69,10 @@ async def get_links(
raise HTTPException(status_code=500, detail="获取链接列表失败") raise HTTPException(status_code=500, detail="获取链接列表失败")
@router.get("/{link_id}", response_model=LinkResponse) @router.get("/{link_id}", response_model=LinkInfo)
async def get_link( async def get_link(
link_id: int, link_service: LinksService = Depends(get_link_service) link_id: int, link_service: LinksService = Depends(get_link_service)
): ) -> LinkInfo:
"""获取单个链接详情""" """获取单个链接详情"""
try: try:
link = await link_service.get_link(str(link_id)) link = await link_service.get_link(str(link_id))
@@ -87,7 +90,7 @@ async def get_link(
@router.delete("/{link_id}") @router.delete("/{link_id}")
async def delete_link( async def delete_link(
link_id: str, link_service: LinksService = Depends(get_link_service) link_id: str, link_service: LinksService = Depends(get_link_service)
): ) -> dict[str, str]:
"""删除链接""" """删除链接"""
try: try:
success = await link_service.delete_link(link_id) success = await link_service.delete_link(link_id)
@@ -108,7 +111,7 @@ async def toggle_link_status(
link_id: str, link_id: str,
status: LinkStatus, status: LinkStatus,
link_service: LinksService = Depends(get_link_service), link_service: LinksService = Depends(get_link_service),
): ) -> LinkInfo:
"""切换链接状态""" """切换链接状态"""
try: try:
updated_link = await link_service.update_link_status(link_id, status) updated_link = await link_service.update_link_status(link_id, status)
@@ -122,3 +125,25 @@ async def toggle_link_status(
except Exception as e: except Exception as e:
logger.error(f"切换链接状态失败: {str(e)}", link_id=link_id, exc_info=True) logger.error(f"切换链接状态失败: {str(e)}", link_id=link_id, exc_info=True)
raise HTTPException(status_code=500, detail="切换链接状态失败") 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="更新链接权重失败")

View File

@@ -12,13 +12,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_async_db from app.core.database import get_async_db
from app.core.log import get_logger from app.core.log import get_logger
from app.models.orders import OrderStatus from app.models.orders import OrderStatus
from app.schemas.link import LinkResponse
from app.schemas.order import ( from app.schemas.order import (
OrderDetailResponse, OrderDetailResponse,
OrderStatsResponse, OrderStatsResponse,
) )
from app.schemas.task import CardInfo, UserInfo, LinkInfo from app.schemas.task import CardInfo, UserInfo, LinkInfo
from app.schemas.user_data import UserInfoResponse
from app.services.order_business_service import OrderService from app.services.order_business_service import OrderService
router = APIRouter() router = APIRouter()
@@ -56,7 +54,8 @@ async def get_orders(
# Convert SQLAlchemy models to Pydantic response schemas # Convert SQLAlchemy models to Pydantic response schemas
result = [] result = []
for order in orders: for order in orders:
link_response = LinkResponse( link_response = LinkInfo(
weight=order.links.weight,
id=order.links.id, id=order.links.id,
url=order.links.url, url=order.links.url,
amount=order.links.amount, amount=order.links.amount,
@@ -79,7 +78,7 @@ async def get_orders(
) )
) )
user_data = UserInfoResponse( user_data = UserInfo(
id=order.user_data.id, id=order.user_data.id,
email=order.user_data.email, email=order.user_data.email,
phone=order.user_data.phone, phone=order.user_data.phone,

View File

@@ -9,10 +9,13 @@ from app.core.database import get_async_db
from app.core.log import get_logger from app.core.log import get_logger
from app.schemas.user_data import ( from app.schemas.user_data import (
UserDataCreate, UserDataCreate,
UserDataListResponse,
UserDataResponse, UserDataResponse,
UserDataUploadResponse, UserDataUploadResponse,
) )
from app.schemas.task import (
BulkDeleteUserDataResponse,
PaginatedResponse,
)
from app.services.user_data_service import UserDataService from app.services.user_data_service import UserDataService
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -79,6 +82,30 @@ async def get_user_data(
raise HTTPException(status_code=500, detail="获取用户数据详情失败") 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="删除用户数据") @router.delete("/{user_id}", summary="删除用户数据")
async def delete_user_data( async def delete_user_data(
user_id: str, service: UserDataService = Depends(get_user_data_service) 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="删除用户数据失败") 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( async def get_user_data_list(
page: int = Query(1, ge=1, description="页码"), page: int = Query(1, ge=1, description="页码"),
size: int = Query(20, ge=1, le=100, description="每页大小"), size: int = Query(20, ge=1, le=100, description="每页大小"),
@@ -202,3 +229,5 @@ async def batch_upload_user_data(
except Exception as e: except Exception as e:
logger.error(f"批量上传用户数据失败: {str(e)}", exc_info=True) logger.error(f"批量上传用户数据失败: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="批量上传用户数据失败") raise HTTPException(status_code=500, detail="批量上传用户数据失败")

View File

@@ -13,7 +13,7 @@ from sqlalchemy.ext.asyncio import (
async_sessionmaker, async_sessionmaker,
create_async_engine, create_async_engine,
) )
from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from app.core.config import get_settings from app.core.config import get_settings

View File

@@ -5,7 +5,7 @@
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from enum import Enum 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 sqlalchemy.orm import Mapped, mapped_column, relationship
from .base import BaseModel from .base import BaseModel
@@ -33,6 +33,7 @@ class Links(BaseModel):
url: Mapped[str] = mapped_column(String(255), nullable=False, comment="链接地址") url: Mapped[str] = mapped_column(String(255), nullable=False, comment="链接地址")
amount: Mapped[float] = mapped_column(Float, 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( status: Mapped[LinkStatus] = mapped_column(
default=LinkStatus.ACTIVE, comment="链接状态" default=LinkStatus.ACTIVE, comment="链接状态"
) )

View File

@@ -7,7 +7,7 @@ from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.log import get_logger 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 from app.repositories.base_repository import BaseRepository
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -142,28 +142,55 @@ class LinkRepository(BaseRepository[Links]):
self, current_position: int = 0 self, current_position: int = 0
) -> tuple[Links, int] | None: ) -> tuple[Links, int] | None:
""" """
从轮询池中获取下一个链接 从轮询池中获取下一个链接(基于权重的轮询算法)
Args: Args:
current_position: 当前位置 current_position: 当前轮询位置
Returns: Returns:
(链接实例, 新位置) 或 None (链接实例, 新位置) 或 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) result = await self.db_session.execute(query)
links = list(result.scalars().all()) links = list(result.scalars().all())
if not links: if not links:
return None return None
# 计算下一个位置 # 计算总权重
next_position = (current_position + 1) % len(links) total_weight = sum(link.weight for link in links)
next_link = links[next_position] 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: 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: async def get_pool_size(self) -> int:
""" """
获取轮询池大小 获取轮询池大小(仅包含激活状态且未软删除的链接)
Returns: 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

View File

@@ -130,3 +130,61 @@ class UserDataRepository(BaseRepository[UserData]):
# 创建新用户 # 创建新用户
new_user = await self.create(**user_data) new_user = await self.create(**user_data)
return new_user, True 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

View File

@@ -1,5 +1,6 @@
""" """
Pydantic schemas for API request/response models Pydantic schemas for API request/response models
统一管理的所有 Pydantic 模型
""" """
from .health import ( from .health import (
@@ -8,39 +9,45 @@ from .health import (
LivenessCheckResponse, LivenessCheckResponse,
ReadinessCheckResponse, ReadinessCheckResponse,
) )
from .link import (
LinkCreate,
LinkListResponse,
LinkPoolResponse,
LinkResponse,
LinkStatsResponse,
LinkUpdate,
)
from .order import (
OrderDetailResponse,
OrderStatsResponse,
UploadUrlRequest,
UploadUrlResponse,
)
from .task import ( from .task import (
BatchProcessRequest, BatchProcessRequest,
CardInfo, CardInfo,
DeleteAllDataResponse,
GiftCardDetailCreate,
GiftCardInfoCreate,
GiftCardInfoResponse,
GiftCardRequest,
GiftCardResponse, GiftCardResponse,
GiftCardSubmissionRequest,
GiftCardSubmissionResponse,
LinkBase,
LinkCreate,
LinkInfo, LinkInfo,
LinkPoolResponse,
LinkStatsResponse,
LinkStatus,
LinkUpdate,
OrderDetailResponse,
OrderStatsResponse,
PaginatedResponse, PaginatedResponse,
ProcessOrderRequest, ProcessOrderRequest,
QueueStatsResponse, QueueStatsResponse,
TaskControlRequest,
TaskControlResponse,
TaskListResponse,
TaskListItem,
TaskStateResponse,
TaskStatusResponse, TaskStatusResponse,
UploadUrlRequest,
UploadUrlResponse,
UserInfo, UserInfo,
WorkerStatusResponse, UserDataBase,
)
from .user_data import (
UserDataCreate, UserDataCreate,
UserDataListResponse,
UserDataResponse, UserDataResponse,
UserDataStatsResponse, UserDataStatsResponse,
UserDataUpdate, UserDataUpdate,
UserDataUploadResponse, UserDataUploadResponse,
WorkerStatusResponse,
) )
__all__ = [ __all__ = [
@@ -49,32 +56,45 @@ __all__ = [
"LinkInfo", "LinkInfo",
"CardInfo", "CardInfo",
"PaginatedResponse", "PaginatedResponse",
"LinkStatus",
# User data schemas
"UserDataBase",
"UserDataCreate",
"UserDataUpdate",
"UserDataResponse",
"UserDataUploadResponse",
"UserDataStatsResponse",
# Link schemas
"LinkBase",
"LinkCreate",
"LinkUpdate",
"LinkPoolResponse",
"LinkStatsResponse",
# Order schemas # Order schemas
"OrderStatsResponse", "OrderStatsResponse",
"OrderDetailResponse", "OrderDetailResponse",
"UploadUrlRequest", "UploadUrlRequest",
"UploadUrlResponse", "UploadUrlResponse",
# Gift card schemas
"GiftCardRequest",
"GiftCardResponse", "GiftCardResponse",
"GiftCardSubmissionRequest",
"GiftCardSubmissionResponse",
"GiftCardInfoCreate",
"GiftCardInfoResponse",
"GiftCardDetailCreate",
# Task schemas # Task schemas
"ProcessOrderRequest", "ProcessOrderRequest",
"BatchProcessRequest", "BatchProcessRequest",
"TaskStatusResponse", "TaskStatusResponse",
"WorkerStatusResponse", "WorkerStatusResponse",
"QueueStatsResponse", "QueueStatsResponse",
# Link schemas "TaskControlRequest",
"LinkCreate", "TaskControlResponse",
"LinkUpdate", "TaskStateResponse",
"LinkResponse", "TaskListResponse",
"LinkListResponse", "TaskListItem",
"LinkPoolResponse", "DeleteAllDataResponse",
"LinkStatsResponse",
# User data schemas
"UserDataCreate",
"UserDataUpdate",
"UserDataResponse",
"UserDataListResponse",
"UserDataUploadResponse",
"UserDataStatsResponse",
# Health schemas # Health schemas
"HealthCheckResponse", "HealthCheckResponse",
"DetailedHealthCheckResponse", "DetailedHealthCheckResponse",

View File

@@ -21,5 +21,3 @@ __all__ = [
"GiftCardDetailCreate", "GiftCardDetailCreate",
] ]
# 为了向后兼容,保留别名
GiftCardInfoResponse = CardInfo

View File

@@ -1,103 +1,27 @@
""" """
链接相关的Pydantic模型 链接相关的Pydantic模型
已迁移到 app.schemas.task 模块中统一管理
""" """
from datetime import datetime # 从统一schema导入所有链接相关模型
from enum import Enum from app.schemas.task import (
LinkBase,
LinkCreate,
LinkInfo,
LinkPoolResponse,
LinkStatsResponse,
LinkUpdate,
LinkStatus,
PaginatedResponse,
)
from pydantic import BaseModel, ConfigDict, Field __all__ = [
"LinkBase",
"LinkCreate",
class LinkStatus(str, Enum): "LinkInfo",
"""链接状态枚举""" "LinkPoolResponse",
"LinkStatsResponse",
ACTIVE = "active" "LinkUpdate",
INACTIVE = "inactive" "LinkStatus",
"PaginatedResponse",
]
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="最大金额")

View File

@@ -1,56 +1,20 @@
""" """
订单相关的Pydantic模型 订单相关的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 __all__ = [
from app.schemas.task import CardInfo, LinkInfo, UserInfo "OrderDetailResponse",
"OrderStatsResponse",
"UploadUrlRequest",
class OrderStatsResponse(BaseModel): "UploadUrlResponse",
"""订单统计响应""" ]
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

View File

@@ -4,14 +4,21 @@
""" """
from datetime import datetime from datetime import datetime
from enum import Enum
from typing import Any, Generic, List, TypeVar 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.enums.task import OrderTaskStatus
from app.models.orders import OrderStatus from app.models.orders import OrderStatus
from app.models.giftcards import GiftCardStatus from app.models.giftcards import GiftCardStatus
from app.models.links import LinkStatus
class LinkStatus(str, Enum):
"""链接状态枚举"""
ACTIVE = "active"
INACTIVE = "inactive"
T = TypeVar("T") T = TypeVar("T")
@@ -101,8 +108,6 @@ class UserInfo(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
# 为了向后兼容,保留 TaskUserInfo 别名
TaskUserInfo = UserInfo
class LinkInfo(BaseModel): class LinkInfo(BaseModel):
@@ -111,6 +116,7 @@ class LinkInfo(BaseModel):
id: str = Field(..., description="链接ID") id: str = Field(..., description="链接ID")
url: str = Field(..., description="链接地址") url: str = Field(..., description="链接地址")
amount: float = Field(..., description="金额") amount: float = Field(..., description="金额")
weight: int = Field(..., description="权重(1-100)")
status: LinkStatus = Field(..., description="链接状态") status: LinkStatus = Field(..., description="链接状态")
created_at: str = Field(description="创建时间") created_at: str = Field(description="创建时间")
updated_at: str = Field(description="更新时间") updated_at: str = Field(description="更新时间")
@@ -118,8 +124,6 @@ class LinkInfo(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
# 为了向后兼容,保留 TaskLinkInfo 别名
TaskLinkInfo = LinkInfo
class CardInfo(BaseModel): class CardInfo(BaseModel):
@@ -137,8 +141,6 @@ class CardInfo(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
# 为了向后兼容,保留 TaskCardInfo 别名
TaskCardInfo = CardInfo
class TaskListItem(BaseModel): class TaskListItem(BaseModel):
@@ -268,3 +270,173 @@ class PaginatedResponse(BaseModel, Generic[T]):
pages: int = Field(..., description="总页数") pages: int = Field(..., description="总页数")
model_config = ConfigDict(from_attributes=True, arbitrary_types_allowed=True) 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

View File

@@ -1,104 +1,27 @@
""" """
用户数据相关的Pydantic模型 用户数据相关的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 __all__ = [
"UserDataBase",
"UserDataCreate",
class UserDataBase(BaseModel): "UserDataUpdate",
"""用户数据基础模型""" "UserDataResponse",
"UserDataUploadResponse",
first_name: str = Field(..., description="名字", max_length=255) "UserDataStatsResponse",
last_name: str = Field(..., description="姓氏", max_length=255) "PaginatedResponse",
email: str = Field(..., description="邮箱", max_length=255) "UserInfo",
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

View File

@@ -16,13 +16,15 @@ from app.models.links import Links
from app.repositories.repository_factory import RepositoryFactory from app.repositories.repository_factory import RepositoryFactory
from app.schemas.link import ( from app.schemas.link import (
LinkCreate, LinkCreate,
LinkListResponse,
LinkPoolResponse, LinkPoolResponse,
LinkResponse,
LinkStatsResponse, LinkStatsResponse,
LinkUpdate, LinkUpdate,
LinkStatus, LinkStatus,
) )
from app.schemas.task import (
LinkInfo,
PaginatedResponse,
)
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -39,7 +41,7 @@ class LinksService:
self.repo_factory = RepositoryFactory(db) self.repo_factory = RepositoryFactory(db)
# 注意这里不再直接获取redis客户端而是在需要时调用get_redis() # 注意这里不再直接获取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( 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}") logger.info(f"创建链接成功: {link.id}")
@@ -64,7 +66,7 @@ class LinksService:
async def update_link_status( async def update_link_status(
self, link_id: str, status: LinkStatus self, link_id: str, status: LinkStatus
) -> LinkResponse | None: ) -> LinkInfo | None:
""" """
更新链接状态 更新链接状态
@@ -83,7 +85,7 @@ class LinksService:
return self._convert_to_response(updated_link) return self._convert_to_response(updated_link)
return None 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( async def update_link(
self, link_id: str, link_data: LinkUpdate self, link_id: str, link_data: LinkUpdate
) -> LinkResponse | None: ) -> LinkInfo | None:
""" """
更新链接 更新链接
@@ -164,7 +166,7 @@ class LinksService:
min_amount: float | None = None, min_amount: float | None = None,
max_amount: float | None = None, max_amount: float | None = None,
url_pattern: str | None = None, url_pattern: str | None = None,
) -> LinkListResponse: ) -> PaginatedResponse[LinkInfo]:
""" """
获取链接列表 获取链接列表
@@ -198,7 +200,7 @@ class LinksService:
total = result.total total = result.total
pages = result.pages pages = result.pages
return LinkListResponse( return PaginatedResponse[LinkInfo](
items=[self._convert_to_response(link) for link in links], items=[self._convert_to_response(link) for link in links],
total=total, total=total,
page=page, page=page,
@@ -215,7 +217,7 @@ class LinksService:
page_links = sorted(page_links, key=lambda x: x.created_at, reverse=True) 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], items=[self._convert_to_response(link) for link in page_links],
total=total, total=total,
page=page, page=page,
@@ -297,7 +299,7 @@ class LinksService:
pool_size = await self.repo_factory.links.get_pool_size() pool_size = await self.repo_factory.links.get_pool_size()
await redis_client.set(self.POOL_SIZE_KEY, str(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: Returns:
链接响应模型 链接响应模型
""" """
return LinkResponse( return LinkInfo(
id=link.id, id=link.id,
url=link.url, url=link.url,
amount=link.amount, amount=link.amount,
weight=link.weight,
status=link.status, status=link.status,
created_at=link.created_at.isoformat(), created_at=link.created_at.isoformat(),
updated_at=link.updated_at.isoformat(), updated_at=link.updated_at.isoformat(),

View File

@@ -14,14 +14,14 @@ from app.core.state_manager import StateType, TaskState, task_state_manager
from app.enums.task import OrderTaskStatus from app.enums.task import OrderTaskStatus
from app.repositories.task_repository import TaskRepository from app.repositories.task_repository import TaskRepository
from app.schemas.task import ( from app.schemas.task import (
CardInfo,
GiftCardSubmissionRequest, GiftCardSubmissionRequest,
GiftCardSubmissionResponse, GiftCardSubmissionResponse,
TaskCardInfo, GiftCardDetailCreate,
TaskLinkInfo, LinkInfo,
TaskListItem, TaskListItem,
TaskListResponse, TaskListResponse,
TaskUserInfo, UserInfo,
GiftCardDetailCreate,
) )
from app.services.gift_card_service import GiftCardService from app.services.gift_card_service import GiftCardService
@@ -132,7 +132,7 @@ class TaskService:
user_info = None user_info = None
if order.user_data: if order.user_data:
user_data = order.user_data user_data = order.user_data
user_info = TaskUserInfo( user_info = UserInfo(
id=user_data.id, id=user_data.id,
first_name=user_data.first_name, first_name=user_data.first_name,
last_name=user_data.last_name, last_name=user_data.last_name,
@@ -150,7 +150,8 @@ class TaskService:
link_info = None link_info = None
if order.links: if order.links:
link = order.links link = order.links
link_info = TaskLinkInfo( link_info = LinkInfo(
weight=link.weight,
id=link.id, id=link.id,
url=link.url, url=link.url,
amount=link.amount, amount=link.amount,
@@ -170,7 +171,7 @@ class TaskService:
if gift_card_list: if gift_card_list:
card_info = [] card_info = []
for gift_card in gift_card_list: for gift_card in gift_card_list:
card_info_item = TaskCardInfo( card_info_item = CardInfo(
id=gift_card.id, id=gift_card.id,
card_code=gift_card.card_code, card_code=gift_card.card_code,
card_value=gift_card.card_value, card_value=gift_card.card_value,

View File

@@ -16,12 +16,14 @@ from app.repositories.repository_factory import RepositoryFactory
from app.schemas.user_data import ( from app.schemas.user_data import (
UserDataBase, UserDataBase,
UserDataCreate, UserDataCreate,
UserDataListResponse,
UserDataResponse, UserDataResponse,
UserDataStatsResponse, UserDataStatsResponse,
UserDataUpdate, UserDataUpdate,
UserDataUploadResponse, UserDataUploadResponse,
UserInfoResponse, )
from app.schemas.task import (
PaginatedResponse,
UserInfo,
) )
@@ -93,7 +95,7 @@ class UserDataService:
return None return None
return self._convert_to_response(user) 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, state: str | None = None,
country: str | None = None, country: str | None = None,
name_pattern: str | None = None, name_pattern: str | None = None,
) -> UserDataListResponse: ) -> PaginatedResponse[UserDataResponse]:
""" """
获取用户数据列表 获取用户数据列表
@@ -213,7 +215,7 @@ class UserDataService:
total = result.total total = result.total
pages = result.pages pages = result.pages
return UserDataListResponse( return PaginatedResponse[UserDataResponse](
items=[self._convert_to_response(user) for user in users], items=[self._convert_to_response(user) for user in users],
total=total, total=total,
page=page, page=page,
@@ -228,7 +230,7 @@ class UserDataService:
page_users = users[start_idx:end_idx] page_users = users[start_idx:end_idx]
pages = (total + size - 1) // size pages = (total + size - 1) // size
return UserDataListResponse( return PaginatedResponse[UserDataResponse](
items=[self._convert_to_response(user) for user in page_users], items=[self._convert_to_response(user) for user in page_users],
total=total, total=total,
page=page, page=page,
@@ -282,7 +284,7 @@ class UserDataService:
updated_at=user.updated_at.isoformat(), 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: Returns:
用户完整信息响应模型 用户完整信息响应模型
""" """
return UserInfoResponse( return UserInfo(
id=user.id, id=user.id,
first_name=user.first_name, first_name=user.first_name,
last_name=user.last_name, last_name=user.last_name,
@@ -307,3 +309,15 @@ class UserDataService:
full_name=user.full_name, full_name=user.full_name,
full_address=user.full_address, 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)

View File

@@ -70,6 +70,43 @@ async def _process_apple_order_async(
"order_id": order_id, "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_key = f"apple_order_processing:{order_id}"
lock = get_lock( lock = get_lock(

20
frontend/.hintrc Normal file
View File

@@ -0,0 +1,20 @@
{
"extends": [
"development"
],
"hints": {
"axe/forms": [
"default",
{
"label": "off"
}
],
"axe/name-role-value": [
"default",
{
"button-name": "off"
}
],
"button-type": "off"
}
}

View File

@@ -1,6 +1,8 @@
"use client"; "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"; import { toast } from "sonner";
// 导入 Tooltip 组件 // 导入 Tooltip 组件
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/animate-ui/base/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/animate-ui/base/tooltip";
@@ -11,11 +13,15 @@ interface LinkItemProps {
link: LinkInfo; link: LinkInfo;
onDelete: (linkId: string) => void; onDelete: (linkId: string) => void;
onToggleStatus?: (linkId: string) => void; onToggleStatus?: (linkId: string) => void;
onUpdateWeight?: (linkId: string, weight: number) => void;
isDeleting?: boolean; isDeleting?: boolean;
isTogglingStatus?: 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显示 // 截断URL显示
const truncateUrl = (url: string, maxLength: number = 30) => { const truncateUrl = (url: string, maxLength: number = 30) => {
if (url.length <= maxLength) return url; 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 statusInfo = getStatusInfo(link.status);
const StatusIcon = statusInfo.icon; 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 justify-between gap-4">
<div className="flex items-center 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"> <div className="flex items-center space-x-1 text-sm">
<DollarSign className="h-4 w-4 text-green-600" /> <DollarSign className="h-4 w-4 text-green-600" />

View File

@@ -2,11 +2,12 @@
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; 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 { import {
useCreateLinkApiV1LinksPost, useCreateLinkApiV1LinksPost,
useDeleteLinkApiV1LinksLinkIdDelete, useDeleteLinkApiV1LinksLinkIdDelete,
useToggleLinkStatusApiV1LinksLinkIdStatusPatch, useToggleLinkStatusApiV1LinksLinkIdStatusPatch,
useUpdateLinkWeightApiV1LinksLinkIdWeightPatch,
useGetLinksApiV1LinksListGet useGetLinksApiV1LinksListGet
} from "@/lib/api/generated/link-management.gen"; } from "@/lib/api/generated/link-management.gen";
import { AppleButton } from "@/components/ui/apple-button"; import { AppleButton } from "@/components/ui/apple-button";
@@ -25,6 +26,8 @@ export function LinkManagement({
refreshEnabled = false, refreshEnabled = false,
refreshInterval = 5000, refreshInterval = 5000,
}: LinkManagementProps) { }: LinkManagementProps) {
const [currentPage, setCurrentPage] = useState(1);
const ITEMS_PER_PAGE = 5;
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [newUrl, setNewUrl] = useState(""); const [newUrl, setNewUrl] = useState("");
const [newAmount, setNewAmount] = useState(""); const [newAmount, setNewAmount] = useState("");
@@ -34,11 +37,15 @@ export function LinkManagement({
const [isDeletingLink, setIsDeletingLink] = useState(false); const [isDeletingLink, setIsDeletingLink] = useState(false);
const [isTogglingStatus, setIsTogglingStatus] = useState(false); const [isTogglingStatus, setIsTogglingStatus] = useState(false);
const [togglingLinkId, setTogglingLinkId] = useState<string | null>(null); 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 // API hooks
const createLinkMutation = useCreateLinkApiV1LinksPost(); const createLinkMutation = useCreateLinkApiV1LinksPost();
const deleteLinkMutation = useDeleteLinkApiV1LinksLinkIdDelete(); const deleteLinkMutation = useDeleteLinkApiV1LinksLinkIdDelete();
const toggleLinkStatusMutation = useToggleLinkStatusApiV1LinksLinkIdStatusPatch(); const toggleLinkStatusMutation = useToggleLinkStatusApiV1LinksLinkIdStatusPatch();
const updateLinkWeightMutation = useUpdateLinkWeightApiV1LinksLinkIdWeightPatch();
// 获取链接列表 // 获取链接列表
const { const {
@@ -48,7 +55,7 @@ export function LinkManagement({
refetch, refetch,
isRefetching isRefetching
} = useGetLinksApiV1LinksListGet( } = useGetLinksApiV1LinksListGet(
{ page: 1, size: 50 }, { page: currentPage, size: ITEMS_PER_PAGE },
{ {
query: { query: {
enabled: true, enabled: true,
@@ -71,12 +78,19 @@ export function LinkManagement({
return; return;
} }
const weight = parseInt(newWeight) || 1;
if (weight < 0 || weight > 100) {
toast.error("权重必须在0-100之间");
return;
}
setIsAddingLink(true); setIsAddingLink(true);
try { try {
await createLinkMutation.mutateAsync({ await createLinkMutation.mutateAsync({
data: { data: {
url: newUrl.trim(), url: newUrl.trim(),
amount: amount amount: amount,
weight: weight
} }
}); });
toast.success("链接添加成功", { toast.success("链接添加成功", {
@@ -86,6 +100,7 @@ export function LinkManagement({
setIsDialogOpen(false); setIsDialogOpen(false);
setNewUrl(""); setNewUrl("");
setNewAmount(""); setNewAmount("");
setNewWeight("1");
refetch(); refetch();
} catch (error) { } catch (error) {
toast.error("添加链接失败", { toast.error("添加链接失败", {
@@ -136,7 +151,7 @@ export function LinkManagement({
const currentLink = linksData?.items?.find(link => link.id === linkId); const currentLink = linksData?.items?.find(link => link.id === linkId);
const currentStatus = currentLink?.status || 'inactive'; const currentStatus = currentLink?.status || 'inactive';
const newStatus = currentStatus === 'active' ? 'inactive' : 'active'; const newStatus = currentStatus === 'active' ? 'inactive' : 'active';
await toggleLinkStatusMutation.mutateAsync({ await toggleLinkStatusMutation.mutateAsync({
linkId: linkId, linkId: linkId,
params: { 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 links = linksData?.items || [];
const total = linksData?.total || 0; const total = linksData?.total || 0;
const totalPages = Math.ceil(total / ITEMS_PER_PAGE);
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -240,6 +275,26 @@ export function LinkManagement({
</p> </p>
</div> </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> </div>
<DialogFooter className="flex gap-2 pt-4"> <DialogFooter className="flex gap-2 pt-4">
@@ -252,7 +307,7 @@ export function LinkManagement({
</AppleButton> </AppleButton>
<AppleButton <AppleButton
onClick={handleAddLink} onClick={handleAddLink}
disabled={isAddingLink || !newUrl.trim() || !newAmount} disabled={isAddingLink || !newUrl.trim() || !newAmount || !newWeight}
className="flex items-center gap-2 apple-glass-button" className="flex items-center gap-2 apple-glass-button"
> >
{isAddingLink ? ( {isAddingLink ? (
@@ -356,8 +411,10 @@ export function LinkManagement({
link={link} link={link}
onDelete={handleDeleteLink} onDelete={handleDeleteLink}
onToggleStatus={handleToggleStatus} onToggleStatus={handleToggleStatus}
onUpdateWeight={handleUpdateWeight}
isDeleting={isDeletingLink} isDeleting={isDeletingLink}
isTogglingStatus={isTogglingStatus && togglingLinkId === link.id} isTogglingStatus={isTogglingStatus && togglingLinkId === link.id}
isUpdatingWeight={isUpdatingWeight && updatingWeightLinkId === link.id}
/> />
))} ))}
</div> </div>
@@ -365,11 +422,74 @@ export function LinkManagement({
)} )}
</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">
( {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 && ( {total > 0 && (
<div className="flex justify-between items-center pt-4 mt-4 border-t border-gray-200 dark:border-gray-700"> <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"> <span className="text-sm text-gray-600 dark:text-gray-400">
{links.length} / {total} {links.length}
</span> </span>
{refreshEnabled && ( {refreshEnabled && (
<span className="text-xs text-gray-500 dark:text-gray-500"> <span className="text-xs text-gray-500 dark:text-gray-500">

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useMemo } from "react";
import { import {
RefreshCw, RefreshCw,
User, User,
@@ -10,7 +10,9 @@ import {
XCircle, XCircle,
AlertCircle, AlertCircle,
Loader2, Loader2,
Gift Gift,
ChevronLeft,
ChevronRight
} from "lucide-react"; } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/animate-ui/base/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/animate-ui/base/tooltip";
import { useGetTaskListApiV1TasksListGet } from "@/lib/api/generated/task-management.gen"; import { useGetTaskListApiV1TasksListGet } from "@/lib/api/generated/task-management.gen";
@@ -30,6 +32,8 @@ interface TaskListProps {
export function TaskList({ refreshEnabled = false, refreshInterval = 5000, className }: TaskListProps) { export function TaskList({ refreshEnabled = false, refreshInterval = 5000, className }: TaskListProps) {
const [isLocalRefreshing, setIsLocalRefreshing] = useState(false); const [isLocalRefreshing, setIsLocalRefreshing] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const ITEMS_PER_PAGE = 12;
// 获取任务列表 // 获取任务列表
const { const {
@@ -56,6 +60,25 @@ export function TaskList({ refreshEnabled = false, refreshInterval = 5000, class
const tasks = taskListData?.tasks || []; 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) => { const truncateErrorMessage = (message: string, maxLength: number = 50) => {
if (message.length <= maxLength) return message; if (message.length <= maxLength) return message;
@@ -179,163 +202,228 @@ export function TaskList({ refreshEnabled = false, refreshInterval = 5000, class
)} )}
{!isLoading && !error && tasks.length > 0 && ( {!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 className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div {paginatedTasks.map((task) => (
key={task.task_id} <div
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" 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 justify-between mb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
{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="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-blue-500"></div> {getStatusIcon(task.status)}
<span className="text-xs text-gray-600 dark:text-gray-400"> <span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{task.user_info.email} {task.task_id.slice(-8)}
</span> </span>
</div> </div>
)} <Badge className={cn("px-2 py-1 rounded-full text-xs font-medium", getStatusColor(task.status))}>
{/* 用户ID */} {getStatusText(task.status)}
{task.user_info?.first_name && ( </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="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-gray-400"></div> <User className="h-4 w-4 text-gray-500" />
<span className="text-xs text-gray-500 dark: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} {task.user_info.first_name} {task.user_info.last_name}
</span> </span>
</div> </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 && ( {task.link_info && (
<div className="space-y-2 mb-3"> <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 justify-between">
<div className="flex items-center gap-1"> <span className="text-xs text-gray-600 dark:text-gray-400">
<span className="text-sm font-medium text-green-600">
${task.link_info.amount || 0} </span>
</span> <span className="text-xs font-medium text-gray-900 dark:text-gray-100">
</div> <AnimatedNumber
value={task.progress}
duration={500}
decimals={1}
/>%
</span>
</div> </div>
{/* 链接 URL */} <Progress
{task.link_info.url && ( value={task.progress}
<div className="flex items-start gap-2"> className={cn(
<Link className="h-3 w-3 text-gray-400 mt-0.5" /> "h-2",
<a task.progress === 100
href={task.link_info.url} ? "[&>[data-slot='progress-indicator']]:bg-green-600"
target="_blank" : task.progress > 0 ? "[&>[data-slot='progress-indicator']]:bg-blue-600" : "[&>[data-slot='progress-indicator']]:bg-gray-400"
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>
}
/> />
</div> </div>
)}
{/* 错误信息 */} {/* 礼品卡输入框 - 只在等待礼品卡状态显示 */}
{task.error_message && ( {task.status === "waiting_gift_card" && task.link_info?.amount && (
<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="mt-4">
<div className="flex items-start gap-2"> <GiftCardInput
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" /> taskId={task.task_id}
<div className="flex-1 min-w-0"> amount={task.link_info.amount}
{task.error_message.length > 50 ? ( updatedAt={task.updated_at}
<Tooltip> onSubmit={(success) => {
<TooltipTrigger> if (success) {
<span className="text-sm text-red-700 dark:text-red-300 cursor-help font-medium break-words"> refetch();
{truncateErrorMessage(task.error_message)} }
</span> }}
</TooltipTrigger> triggerButton={
<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"> <AppleButton variant="default" size="sm" className="w-full flex items-center gap-2 apple-glass-button">
<p className="max-w-xs text-sm font-medium leading-relaxed break-words">{task.error_message}</p> <Gift className="h-4 w-4" />
</TooltipContent> (${task.link_info.amount})
</Tooltip> </AppleButton>
) : ( }
<span className="text-sm text-red-700 dark:text-red-300 font-medium break-words"> />
{task.error_message} </div>
</span> )}
)}
{/* 错误信息 */}
{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>
)}
</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> </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> </>
)} )}
</div> </div>
@@ -343,7 +431,7 @@ export function TaskList({ refreshEnabled = false, refreshInterval = 5000, class
{!isLoading && !error && ( {!isLoading && !error && (
<div className="flex justify-between items-center pt-4 mt-4 border-t border-gray-200 dark:border-gray-700"> <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"> <span className="text-sm text-gray-600 dark:text-gray-400">
{tasks.length} {paginatedTasks.length} ( {tasks.length} )
</span> </span>
{refreshEnabled && ( {refreshEnabled && (
<span className="text-xs text-gray-500 dark:text-gray-500"> <span className="text-xs text-gray-500 dark:text-gray-500">

View File

@@ -3,18 +3,19 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { AppleButton } from "@/components/ui/apple-button"; import { AppleButton } from "@/components/ui/apple-button";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; 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 { Badge } from "../ui/badge";
import { BrushCleaning } from "../animate-ui/icons/brush-cleaning";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/animate-ui/base/tooltip'; } 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 { 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 { FileUploadModal } from "@/components/forms/file-upload-modal";
import { GetUserDataListApiV1UserDataListGetParams } from "@/lib/api/generated/schemas"; import { GetUserDataListApiV1UserDataListGetParams } from "@/lib/api/generated/schemas";
@@ -37,11 +38,14 @@ const userDataService = {
export function UploadedDataDisplay({ refreshEnabled = true, refreshInterval = 5000 }: UploadedDataDisplayProps) { export function UploadedDataDisplay({ refreshEnabled = true, refreshInterval = 5000 }: UploadedDataDisplayProps) {
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const { data: userDataList, isLoading, refetch } = userDataService.useUserDataList({ const { data: userDataList, isLoading, refetch } = userDataService.useUserDataList({
page: 1, page: 1,
size: 10 size: 10
}); });
const bulkDeleteMutation = useBulkDeleteAllUserDataApiV1UserDataAllDelete();
// 获取数据 // 获取数据
const fetchUploadedData = async () => { const fetchUploadedData = async () => {
try { try {
@@ -73,16 +77,20 @@ export function UploadedDataDisplay({ refreshEnabled = true, refreshInterval = 5
setIsUploadModalOpen(false); setIsUploadModalOpen(false);
}; };
// 清空数据 // 处理删除所有数据
const handleClearData = async () => { const handleDeleteAllData = async () => {
try { try {
console.log("数据清空功能待实现"); await bulkDeleteMutation.mutateAsync({});
await fetchUploadedData(); await refetch();
} catch { setIsDeleteDialogOpen(false);
console.error("清空数据失败"); toast.success("所有用户数据已成功删除");
} catch (error) {
console.error("删除所有数据失败:", error);
toast.error("删除数据失败,请稍后重试");
} }
}; };
return ( return (
<> <>
<div className="apple-glass-card rounded-2xl p-6 transition-all duration-300"> <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} {userDataList?.total}
</Badge> </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}> <Dialog open={isUploadModalOpen} onOpenChange={setIsUploadModalOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<AppleButton variant="default" size="sm" className="flex items-center gap-2 apple-glass-button"> <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>
</div> </div>
) : userDataList?.items.length ? ( ) : userDataList?.items.length ? (
<ScrollArea className="h-[150px] max-h-[200px] w-full rounded-md"> <ScrollArea className="h-[300px] w-full rounded-lg">
<div className="p-4 space-y-2"> <div className="p-4 space-y-4">
{userDataList.items.map((item) => ( {userDataList.items.map((item) => (
<TooltipProvider key={item.id}> <TooltipProvider key={item.id}>
<Tooltip hoverable> <Tooltip hoverable>
<TooltipTrigger render={<div <TooltipTrigger render={
key={item.id} <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">
className="flex justify-between items-center p-2 rounded-md bg-card hover:bg-secondary/50 group" {/* 用户头像和基本信息 */}
> <div className="flex items-start gap-2 mb-3">
<div className="text-sm font-mono truncate max-w-[100%]"> <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} {item.last_name} {item.street_address} {item.city} {item.state} {item.zip_code} {item.email} {item.phone} {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>
</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> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
))} ))}
</div> </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> </ScrollArea>
) : ( ) : (
<div className="text-center py-8 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground">
@@ -152,17 +273,6 @@ export function UploadedDataDisplay({ refreshEnabled = true, refreshInterval = 5
</div> </div>
)} )}
</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> </div>
{/* 文件上传模态框 */} {/* 文件上传模态框 */}

View File

@@ -22,12 +22,14 @@ import type {
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import type { import type {
DeleteLinkApiV1LinksLinkIdDelete200,
GetLinksApiV1LinksListGetParams, GetLinksApiV1LinksListGetParams,
HTTPValidationError, HTTPValidationError,
LinkCreate, LinkCreate,
LinkInfo, LinkInfo,
PaginatedResponseLinkInfo, PaginatedResponseLinkInfo,
ToggleLinkStatusApiV1LinksLinkIdStatusPatchParams, ToggleLinkStatusApiV1LinksLinkIdStatusPatchParams,
UpdateLinkWeightApiV1LinksLinkIdWeightPatchParams,
} from "./schemas"; } from "./schemas";
import { axiosClient } from "../enhanced-axios-client"; import { axiosClient } from "../enhanced-axios-client";
@@ -476,7 +478,7 @@ const deleteLinkApiV1LinksLinkIdDelete = (
linkId: string, linkId: string,
options?: SecondParameter<typeof axiosClient>, options?: SecondParameter<typeof axiosClient>,
) => { ) => {
return axiosClient<unknown>( return axiosClient<DeleteLinkApiV1LinksLinkIdDelete200>(
{ url: `/api/v1/links/${linkId}`, method: "DELETE" }, { url: `/api/v1/links/${linkId}`, method: "DELETE" },
{ second: true, ...options }, { second: true, ...options },
); );
@@ -563,7 +565,7 @@ const toggleLinkStatusApiV1LinksLinkIdStatusPatch = (
params: ToggleLinkStatusApiV1LinksLinkIdStatusPatchParams, params: ToggleLinkStatusApiV1LinksLinkIdStatusPatchParams,
options?: SecondParameter<typeof axiosClient>, options?: SecondParameter<typeof axiosClient>,
) => { ) => {
return axiosClient<unknown>( return axiosClient<LinkInfo>(
{ url: `/api/v1/links/${linkId}/status`, method: "PATCH", params }, { url: `/api/v1/links/${linkId}/status`, method: "PATCH", params },
{ second: true, ...options }, { second: true, ...options },
); );
@@ -656,3 +658,105 @@ export const useToggleLinkStatusApiV1LinksLinkIdStatusPatch = <
return useMutation(mutationOptions, queryClient); 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);
};

View File

@@ -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;
};

View File

@@ -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;
}

View File

@@ -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 };

View File

@@ -6,10 +6,13 @@
* OpenAPI spec version: 2.0.0 * OpenAPI spec version: 2.0.0
*/ */
export * from "./bulkDeleteAllUserDataApiV1UserDataAllDeleteParams";
export * from "./bulkDeleteUserDataResponse";
export * from "./cardInfo"; export * from "./cardInfo";
export * from "./cardInfoFailureReason"; export * from "./cardInfoFailureReason";
export * from "./deleteAllDataResponse"; export * from "./deleteAllDataResponse";
export * from "./deleteAllDataResponseDeletedTables"; export * from "./deleteAllDataResponseDeletedTables";
export * from "./deleteLinkApiV1LinksLinkIdDelete200";
export * from "./exportOrdersApiV1OrdersExportGetParams"; export * from "./exportOrdersApiV1OrdersExportGetParams";
export * from "./getLinksApiV1LinksListGetParams"; export * from "./getLinksApiV1LinksListGetParams";
export * from "./getOrdersApiV1OrdersListGetParams"; export * from "./getOrdersApiV1OrdersListGetParams";
@@ -53,6 +56,7 @@ export * from "./taskListItemWorkerId";
export * from "./taskListResponse"; export * from "./taskListResponse";
export * from "./taskStateResponse"; export * from "./taskStateResponse";
export * from "./toggleLinkStatusApiV1LinksLinkIdStatusPatchParams"; export * from "./toggleLinkStatusApiV1LinksLinkIdStatusPatchParams";
export * from "./updateLinkWeightApiV1LinksLinkIdWeightPatchParams";
export * from "./userDataCreate"; export * from "./userDataCreate";
export * from "./userDataResponse"; export * from "./userDataResponse";
export * from "./userDataUploadResponse"; export * from "./userDataUploadResponse";

View File

@@ -20,6 +20,12 @@ export interface LinkCreate {
* 金额 * 金额
*/ */
amount: number; amount: number;
/**
* 权重(1-100)
* @minimum 1
* @maximum 100
*/
weight?: number;
/** 链接状态 */ /** 链接状态 */
status?: LinkStatus; status?: LinkStatus;
} }

View File

@@ -17,6 +17,8 @@ export interface LinkInfo {
url: string; url: string;
/** 金额 */ /** 金额 */
amount: number; amount: number;
/** 权重(1-100) */
weight: number;
/** 链接状态 */ /** 链接状态 */
status: LinkStatus; status: LinkStatus;
/** 创建时间 */ /** 创建时间 */

View File

@@ -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;
};

View File

@@ -22,6 +22,8 @@ import type {
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import type { import type {
BulkDeleteAllUserDataApiV1UserDataAllDeleteParams,
BulkDeleteUserDataResponse,
GetUserDataListApiV1UserDataListGetParams, GetUserDataListApiV1UserDataListGetParams,
HTTPValidationError, HTTPValidationError,
PaginatedResponseUserDataResponse, PaginatedResponseUserDataResponse,
@@ -906,3 +908,95 @@ export const useBatchUploadUserDataApiV1UserDataBatchUploadPost = <
return useMutation(mutationOptions, queryClient); 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);
};