mirror of
https://git.oceanpay.cc/danial/kami_apple_exchage.git
synced 2025-12-18 21:23:49 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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'"
|
||||
@@ -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
|
||||
|
||||
@@ -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"订单处理完成")
|
||||
|
||||
|
||||
@@ -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}"],
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
--gradient-2: linear-gradient(to right, #1a1f2c, #252d3c);
|
||||
--gradient-3: linear-gradient(to right, #1c2333, #232b3b);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
@@ -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>
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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],
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface CrawlerItem {
|
||||
zip_code: string;
|
||||
phone: string;
|
||||
timestamp: string;
|
||||
final_order_url: string;
|
||||
}
|
||||
|
||||
export interface UploadedData {
|
||||
|
||||
Reference in New Issue
Block a user