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
|
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
|
||||||
|
|||||||
@@ -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="更新链接权重失败")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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="批量上传用户数据失败")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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="链接状态"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -21,5 +21,3 @@ __all__ = [
|
|||||||
"GiftCardDetailCreate",
|
"GiftCardDetailCreate",
|
||||||
]
|
]
|
||||||
|
|
||||||
# 为了向后兼容,保留别名
|
|
||||||
GiftCardInfoResponse = CardInfo
|
|
||||||
|
|||||||
@@ -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="最大金额")
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
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";
|
"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" />
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
{/* 文件上传模态框 */}
|
{/* 文件上传模态框 */}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
|
* 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";
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ export interface LinkCreate {
|
|||||||
* 金额
|
* 金额
|
||||||
*/
|
*/
|
||||||
amount: number;
|
amount: number;
|
||||||
|
/**
|
||||||
|
* 权重(1-100)
|
||||||
|
* @minimum 1
|
||||||
|
* @maximum 100
|
||||||
|
*/
|
||||||
|
weight?: number;
|
||||||
/** 链接状态 */
|
/** 链接状态 */
|
||||||
status?: LinkStatus;
|
status?: LinkStatus;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export interface LinkInfo {
|
|||||||
url: string;
|
url: string;
|
||||||
/** 金额 */
|
/** 金额 */
|
||||||
amount: number;
|
amount: number;
|
||||||
|
/** 权重(1-100) */
|
||||||
|
weight: number;
|
||||||
/** 链接状态 */
|
/** 链接状态 */
|
||||||
status: LinkStatus;
|
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";
|
} 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);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user