feat: 更新订单结果模型和前端展示

- 在 database.py 中新增 final_order_url 字段以存储最终订单链接,并添加更新方法
- 修改 simple_app.py 和 test.py,确保在处理订单时更新 final_order_url
- 更新前端组件,展示 final_order_url,并添加工具提示以改善用户体验
- 调整 run.bat 文件以使用虚拟环境中的 Python 解释器
- 更新 eslint 配置,忽略特定配置文件
This commit is contained in:
danial
2025-08-12 19:32:21 +08:00
parent 4487ce1076
commit e145c72c5f
11 changed files with 189 additions and 179 deletions

View File

@@ -194,6 +194,7 @@ class OrderResult(Base):
Enum(OrderResultStatus), default=OrderResultStatus.PENDING
)
order_number: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
final_order_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
failure_reason: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
@@ -829,6 +830,19 @@ class ResultDB(Singleton):
except Exception as e:
logger.error(f"绑定线程数据时出错: {e}")
def update_final_order_url(
self, order_result_id: int, final_order_url: str
) -> None:
"""更新最终订单URL"""
try:
with self.__db_manager.get_session() as session:
session.query(OrderResult).filter(
OrderResult.id == order_result_id
).update({OrderResult.final_order_url: final_order_url})
session.commit()
except Exception as e:
logger.error(f"更新最终订单URL时出错: {e}")
def get_one(self, id_: int) -> Optional[OrderResult]:
"""读取订单结果"""
try:
@@ -846,7 +860,7 @@ class ResultDB(Singleton):
logger.error(f"读取订单结果时出错: {e}")
return None
def add_order_number(self, order: OrderResult, order_number: str) -> None:
def update_order_number(self, order: OrderResult, order_number: str) -> None:
"""写入订单号"""
try:
with self.__db_manager.get_session() as session:
@@ -935,20 +949,13 @@ class ThreadDataDB(Singleton):
session.add(
ThreadData(thread_id=thread_id, status=ThreadStatus.PENDING)
)
if renew:
if thread_data:
thread_data.status = ThreadStatus.PENDING
thread_data.bind_gift_card_time = None
thread_data.tmp_order_result_id = None
thread_data.bind_gift_card_duration = 0
thread_data.gift_card_number = None
thread_data.progress = 0.0
else:
session.add(
ThreadData(
thread_id=thread_id, status=ThreadStatus.PENDING
)
)
if renew and thread_data:
thread_data.status = ThreadStatus.PENDING
thread_data.bind_gift_card_time = None
thread_data.tmp_order_result_id = None
thread_data.bind_gift_card_duration = 0
thread_data.gift_card_number = None
thread_data.progress = 0.0
session.commit()
logger.info(f"删除线程数据成功: {thread_ids}")
except Exception as e:

View File

@@ -1,2 +1,2 @@
powershell -Command "Start-Process python -ArgumentList 'test.py'"
powershell -Command "Start-Process python -ArgumentList 'simple_app.py'"
powershell -Command "Start-Process .\.venv\Scripts\python.exe -ArgumentList 'test.py'"
powershell -Command "Start-Process .\.venv\Scripts\python.exe -ArgumentList 'simple_app.py'"

View File

@@ -108,6 +108,7 @@ def get_result_raw():
"state": result.user_data.state,
"zip_code": result.user_data.zip_code,
"order_number": result.order_number,
"final_order_url": result.final_order_url,
"order_url": (
result.upload_config.url if result.upload_config else ""
),
@@ -143,12 +144,12 @@ def download_excel():
formatted_lines.append(
{
"id": result.id,
"gift_card_number": (
", ".join(gift_card_numbers) if gift_card_numbers else ""
),
"order_number": result.order_number,
"status": result.status.value,
"order_number": result.order_number,
"final_order_url": result.final_order_url,
"email": result.user_data.email,
"phone": result.user_data.phone,
"first_name": result.user_data.first_name,
@@ -157,34 +158,17 @@ def download_excel():
"city": result.user_data.city,
"state": result.user_data.state,
"zip_code": result.user_data.zip_code,
"failure_reason": result.failure_reason,
"timestamp": result.created_at.strftime("%Y-%m-%d %H:%M:%S"),
"order_url": (
result.upload_config.url if result.upload_config else ""
),
"failure_reason": result.failure_reason,
"timestamp": result.created_at.strftime("%Y-%m-%d %H:%M:%S"),
}
)
# 创建DataFrame并生成Excel文件
if formatted_lines:
df = pd.DataFrame(formatted_lines)
df.columns = [
"id",
"gift_card_number",
"timestamp",
"status",
"email",
"phone",
"first_name",
"last_name",
"street_address",
"city",
"state",
"zip_code",
"order_number",
"order_url",
"failure_reason",
]
# 创建临时文件
with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp_file:
excel_path = tmp_file.name

View File

@@ -702,6 +702,8 @@ def process_single_order(thread_id, order_result: OrderResult):
gift_card_data,
OrderResultStatus.SUCCESS,
)
ResultDB().update_order_number(order_result, order_number_text)
ResultDB().update_final_order_url(order_result.id, order_number_link)
except Exception as order_extract_error:
logger.error(f"提取订单信息失败: {order_extract_error}")
@@ -712,7 +714,8 @@ def process_single_order(thread_id, order_result: OrderResult):
gift_card_data,
OrderResultStatus.SUCCESS,
)
ResultDB().add_order_number(order_result, order_number)
ResultDB().update_order_number(order_result, order_number)
ResultDB().update_final_order_url(order_result.id, order_url)
logger.info(f"订单处理完成")

View File

@@ -7,6 +7,13 @@ const compat = new FlatCompat({
});
export default [
{
ignores: [
"tailwind.config.js",
"postcss.config.js",
"next.config.js",
]
},
...compat.extends("next/core-web-vitals"),
{
files: ["**/*.{js,jsx,ts,tsx}"],

View File

@@ -70,8 +70,6 @@
--gradient-2: linear-gradient(to right, #1a1f2c, #252d3c);
--gradient-3: linear-gradient(to right, #1c2333, #232b3b);
}
}
@layer base {

View File

@@ -2,17 +2,20 @@
import { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { CrawlerItem, crawlerService } from "@/lib/api/crawler-service";
import { ScrollArea, ScrollBar } from "../ui/scroll-area";
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "../animate-ui/headless/tabs";
import { RippleButton } from "../animate-ui/buttons/ripple";
import { ColumnDef } from "@tanstack/react-table"
import { DataTable } from "./item-list-date-table";
import { Badge } from "../ui/badge";
import { useInterval } from "@/lib/hooks/use-timeout";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../animate-ui/base/tooltip";
const columns: ColumnDef<CrawlerItem>[] = [
{
accessorKey: "gift_card_number",
header: "Gift Card Number",
},
{
accessorKey: "status",
header: "Status",
@@ -24,6 +27,30 @@ const columns: ColumnDef<CrawlerItem>[] = [
accessorKey: "order_number",
header: "Order Number",
},
{
accessorKey: "final_order_url",
header: "Final Order URL",
cell: ({ row }) => {
return <TooltipProvider>
<Tooltip hoverable>
<TooltipTrigger render={<div
className="flex justify-between items-center p-2 rounded-md bg-background hover:bg-secondary/50 group"
>
<div className="text-sm font-mono truncate max-w-[100%]">{row.original.final_order_url}</div>
</div>} />
<TooltipContent>
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
{row.original.final_order_url.length > 20 ? row.original.final_order_url.slice(0, 20) + "..." : row.original.final_order_url}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
},
},
{
accessorKey: "phone",
header: "Phone",
},
{
accessorKey: "first_name",
header: "First Name",
@@ -48,10 +75,6 @@ const columns: ColumnDef<CrawlerItem>[] = [
accessorKey: "email",
header: "Email",
},
{
accessorKey: "gift_card_number",
header: "Gift Card Number",
},
{
accessorKey: "state",
header: "State",
@@ -60,10 +83,6 @@ const columns: ColumnDef<CrawlerItem>[] = [
accessorKey: "zip_code",
header: "Zip Code",
},
{
accessorKey: "phone",
header: "Phone",
},
{
accessorKey: "timestamp",
header: "Timestamp",
@@ -75,6 +94,22 @@ const columns: ColumnDef<CrawlerItem>[] = [
{
accessorKey: "order_url",
header: "Order URL",
cell: ({ row }) => {
return <TooltipProvider>
<Tooltip hoverable>
<TooltipTrigger render={<div
className="flex justify-between items-center p-2 rounded-md bg-background hover:bg-secondary/50 group"
>
<div className="text-sm font-mono truncate max-w-[100%]">{row.original.order_url}</div>
</div>} />
<TooltipContent>
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
{row.original.order_url.length > 20 ? row.original.order_url.slice(0, 20) + "..." : row.original.order_url}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
},
},
]

View File

@@ -285,110 +285,112 @@ export function ThreadMonitor({ refreshInterval = 5000, refreshEnabled = true }:
</div>
) : (
<ScrollArea className="max-h-[600px] w-full space-y-4">
{threads.map((thread) => {
const statusInfo = getThreadStatusInfo(thread.status);
return (
<div key={thread.id} className="space-y-3 p-4 border rounded-lg bg-card">
{/* 线程头部信息 */}
<div className="flex justify-between items-center">
<div className="flex items-center space-x-2">
{/* 状态 */}
<Badge
className={statusInfo.color}
variant="secondary"
>
<div className="flex items-center">
{statusInfo.icon}
{statusInfo.label}
</div>
</Badge>
<span className="font-medium">线{thread.thread_id}</span>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="icon"
onClick={() => toggleThreadStatus(thread.thread_id, thread.status)}
className="h-5 w-5"
>
{thread.status !== 'pause' ? <Play size={1} /> : <Pause size={1} />}
</Button>
<Button
variant="outline"
size="icon"
onClick={() => openUploadUrlDialog(thread.thread_id)}
className="h-5 w-5"
>
<Settings size={1} />
</Button>
</div>
</div>
{/* 线程状态内容 */}
{thread.status === 'pending' || thread.status === 'stopped' || thread.status === 'pause' ? (
// 未运行状态
<div className="space-y-2">
<div className="text-sm text-muted-foreground">
{thread.status === 'pending' ? '等待任务' : '暂停任务'}
<div className="flex flex-col gap-1">
{threads.map((thread) => {
const statusInfo = getThreadStatusInfo(thread.status);
return (
<div key={thread.id} className="p-4 border rounded-lg bg-card">
{/* 线程头部信息 */}
<div className="flex justify-between items-center">
<div className="flex items-center space-x-2">
{/* 状态 */}
<Badge
className={statusInfo.color}
variant="secondary"
>
<div className="flex items-center">
{statusInfo.icon}
{statusInfo.label}
</div>
</Badge>
<span className="font-medium">线{thread.thread_id}</span>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="icon"
onClick={() => toggleThreadStatus(thread.thread_id, thread.status)}
className="h-5 w-5"
>
{thread.status !== 'pause' ? <Play size={1} /> : <Pause size={1} />}
</Button>
<Button
variant="outline"
size="icon"
onClick={() => openUploadUrlDialog(thread.thread_id)}
className="h-5 w-5"
>
<Settings size={1} />
</Button>
</div>
</div>
) : (
// 运行状态或等待卡密状态
<div className="space-y-3">
{/* 当前用户名 */}
{thread.username && (
<>
<div className="flex items-center space-x-2 text-sm">
<User className="h-4 w-4" />
<span className="text-sm">
{thread.username}
</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<Mail className="h-4 w-4 text-gray-500" />
<span className="text-gray-700 dark:text-gray-300">
{thread.email}
</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<Clock className="h-4 w-4 text-gray-500" />
<span className="text-gray-700 dark:text-gray-300">
{thread.bing_gift_card_time}
</span>
</div>
</>
)}
{/* 倒计时(等待卡密状态) */}
{thread.status === 'waiting_card_key' &&
thread.bind_gift_card_duration !== 0 && (
{/* 线程状态内容 */}
{thread.status === 'pending' || thread.status === 'stopped' || thread.status === 'pause' ? (
// 未运行状态
<div className="space-y-2">
<div className="text-sm text-muted-foreground">
{thread.status === 'pending' ? '等待任务' : '暂停任务'}
</div>
</div>
) : (
// 运行状态或等待卡密状态
<div className="space-y-3">
{/* 当前用户名 */}
{thread.username && (
<>
<CountdownTimer
time={(new Date().getTime() - new Date(thread.bing_gift_card_time).getTime()) / 1000}
totalTime={thread.bind_gift_card_duration}
/>
<SubmitGiftCard threadId={thread.thread_id} onSubmit={() => {
fetchThreads();
}} />
<div className="flex items-center space-x-2 text-sm">
<User className="h-4 w-4" />
<span className="text-sm">
{thread.username}
</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<Mail className="h-4 w-4 text-gray-500" />
<span className="text-gray-700 dark:text-gray-300">
{thread.email}
</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<Clock className="h-4 w-4 text-gray-500" />
<span className="text-gray-700 dark:text-gray-300">
{thread.bing_gift_card_time}
</span>
</div>
</>
)}
{/* 进度条 */}
{thread.progress !== undefined && thread.status !== 'waiting_card_key' && (
<div className="space-y-2">
<Progress value={thread.progress} className="h-2" />
</div>
)}
{/* 倒计时(等待卡密状态) */}
{thread.status === 'waiting_card_key' &&
thread.bind_gift_card_duration !== 0 && (
<>
<CountdownTimer
time={(new Date().getTime() - new Date(thread.bing_gift_card_time).getTime()) / 1000}
totalTime={thread.bind_gift_card_duration}
/>
<SubmitGiftCard threadId={thread.thread_id} onSubmit={() => {
fetchThreads();
}} />
</>
)}
</div>
)}
{/* 当前链接 */}
{thread.order_url && (
<UrlDisplay url={thread.order_url} />
)}
</div>
);
})}
{/* 进度条 */}
{thread.progress !== undefined && thread.status !== 'waiting_card_key' && (
<div className="space-y-2">
<Progress value={thread.progress} className="h-2" />
</div>
)}
</div>
)}
{/* 当前链接 */}
{thread.order_url && (
<UrlDisplay url={thread.order_url} />
)}
</div>
);
})}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
)}

View File

@@ -63,7 +63,7 @@ export function UploadedDataDisplay({ refreshEnabled = true, refreshInterval = 5
<CardHeader className="flex items-center justify-between">
<CardTitle className="flex items-center">
<Database className="mr-2 h-5 w-5" />
</CardTitle>
<Badge className="ml-2 bg-blue-500 text-white">
{uploadedData.count}

View File

@@ -17,7 +17,7 @@ export function ThemeTransition() {
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
{/* 主过渡圆圈 - 简化版本,与页面模糊效果保持一致 */}
{/* 主过渡圆圈(精简) */}
<motion.div
className="absolute rounded-full"
style={{
@@ -34,38 +34,11 @@ export function ThemeTransition() {
}}
animate={{
scale: 1,
width: "400vw",
height: "400vw",
width: "360vw",
height: "360vw",
}}
transition={{
duration: 1.2,
ease: [0.4, 0, 0.2, 1],
}}
/>
{/* 辅助光晕效果 - 与页面背景渐变保持一致 */}
<motion.div
className="absolute rounded-full"
style={{
left: clickPosition.x + 36,
top: clickPosition.y + 36,
background: targetTheme === "dark"
? "radial-gradient(circle, rgba(120,119,198,0.3) 0%, rgba(255,119,198,0.2) 30%, rgba(120,219,255,0.1) 60%, transparent 100%)"
: "radial-gradient(circle, rgba(120,119,198,0.2) 0%, rgba(255,119,198,0.15) 30%, rgba(120,219,255,0.1) 60%, transparent 100%)",
}}
initial={{
scale: 0,
width: 0,
height: 0,
}}
animate={{
scale: 1,
width: "350vw",
height: "350vw",
}}
transition={{
duration: 1.0,
delay: 0.2,
duration: 0.7,
ease: [0.4, 0, 0.2, 1],
}}
/>

View File

@@ -54,6 +54,7 @@ export interface CrawlerItem {
zip_code: string;
phone: string;
timestamp: string;
final_order_url: string;
}
export interface UploadedData {