jerry 2 miesięcy temu
rodzic
commit
9a44984597

+ 10 - 10
app/api/router.py

@@ -31,7 +31,6 @@ from app.schemas.card import CardCreate, CardOut
 from app.schemas.task import TaskCreate, TaskOut, TaskUpdate
 from app.schemas.task import TaskCreate, TaskOut, TaskUpdate
 from app.schemas.short_url import ShortUrlCreate, ShortUrlOut
 from app.schemas.short_url import ShortUrlCreate, ShortUrlOut
 from app.schemas.http_session import HttpSessionCreate, HttpSessionUpdate,HttpSessionOut
 from app.schemas.http_session import HttpSessionCreate, HttpSessionUpdate,HttpSessionOut
-from app.schemas.fake import FakeUser
 from app.schemas.auth import SendBindCodeRequest, SendResetCodeRequest, BindEmailRequest, ResetPasswordRequest, LoginRequest, LoginData, AutoRegisterRequest, AutoRegisterData
 from app.schemas.auth import SendBindCodeRequest, SendResetCodeRequest, BindEmailRequest, ResetPasswordRequest, LoginRequest, LoginData, AutoRegisterRequest, AutoRegisterData
 from app.schemas.user import VasUserCreate, VasUserUpdate, VasUserSetProfiles, VasUserOut
 from app.schemas.user import VasUserCreate, VasUserUpdate, VasUserSetProfiles, VasUserOut
 from app.schemas.product import VasProductCreate, VasProductUpdate, VasProductOut
 from app.schemas.product import VasProductCreate, VasProductUpdate, VasProductOut
@@ -65,7 +64,7 @@ from app.services.task_service import TaskService
 from app.services.card_service import CardService
 from app.services.card_service import CardService
 from app.services.seaweedfs_service import SeaweedFSService
 from app.services.seaweedfs_service import SeaweedFSService
 from app.services.http_session_service import HttpSessionService
 from app.services.http_session_service import HttpSessionService
-from app.services.fake_service import generate_fake_users
+from app.services.fake_service import FakeService
 from app.services.auth_service import AuthService
 from app.services.auth_service import AuthService
 from app.services.user_service import UserService
 from app.services.user_service import UserService
 from app.services.product_service import ProductService
 from app.services.product_service import ProductService
@@ -717,13 +716,15 @@ async def cards_view_paginated2(
     obj = await CardService.list_by_keyword(db, keyword, page, size, culture)
     obj = await CardService.list_by_keyword(db, keyword, page, size, culture)
     return success(data=obj)
     return success(data=obj)
 
 
-@admin_required_router.get("/fake/users", summary="生成虚假的预约人信息", tags=["数据生成"], response_model=ApiResponse[List[FakeUser]])
-async def fake_generate_fake_users(
+@admin_required_router.get("/fake/orders", summary="生成虚假的订单信息", tags=["数据生成"], response_model=ApiResponse[List[VasOrderOut]])
+async def fake_generate_fake_orders(
     num: int = Query(1, description="生成几个数据"),
     num: int = Query(1, description="生成几个数据"),
-    living_country = Query("Ireland", description="居住在哪个国家, China, India, United Kingdom, Ireland"),
+    product_id = Query(1, description="商品Id"),
+    db: AsyncSession = Depends(get_db),
+    current_user: VasUser = Depends(get_current_user),
 ):
 ):
-    obj = generate_fake_users(num, living_country=living_country)
-    return success(data=obj)
+    orders = await FakeService.generate_orders(db, num, product_id, current_user)
+    return success(data=orders)
 
 
 @public_router.get("/slots/latest", summary="查询最近的slot", tags=["Slot数据"], response_model=ApiResponse[SlotSnapshotOut])
 @public_router.get("/slots/latest", summary="查询最近的slot", tags=["Slot数据"], response_model=ApiResponse[SlotSnapshotOut])
 async def slots_latest_get(
 async def slots_latest_get(
@@ -1065,8 +1066,7 @@ async def vas_order_create(
 async def vas_order_create_by_admin(
 async def vas_order_create_by_admin(
     payload: VasOrderCreate,
     payload: VasOrderCreate,
     current_user: VasUser = Depends(get_current_user),
     current_user: VasUser = Depends(get_current_user),
-    db: AsyncSession = Depends(get_db),
-    redis_client: Redis = Depends(get_redis_client)
+    db: AsyncSession = Depends(get_db)
 ):
 ):
     product = await ProductService.get(db, payload.product_id)
     product = await ProductService.get(db, payload.product_id)
     # ① 获取产品绑定的 schema
     # ① 获取产品绑定的 schema
@@ -1076,7 +1076,7 @@ async def vas_order_create_by_admin(
         schema_json=schema.schema_json,
         schema_json=schema.schema_json,
         user_inputs=payload.user_inputs,
         user_inputs=payload.user_inputs,
     )
     )
-    created_order = await OrderService.create_by_admin(db, payload, product, current_user, redis_client)
+    created_order = await OrderService.create_by_admin(db, payload, product, current_user)
     return success(data=created_order)
     return success(data=created_order)
 
 
 @admin_required_router.post("/vas/order/adjust-price", summary="管理员调整订单价格", tags=["Visafly签证系统"], response_model=ApiResponse[VasOrderOut])
 @admin_required_router.post("/vas/order/adjust-price", summary="管理员调整订单价格", tags=["Visafly签证系统"], response_model=ApiResponse[VasOrderOut])

+ 0 - 44
app/schemas/fake.py

@@ -1,44 +0,0 @@
-from pydantic import BaseModel
-from typing import Optional
-from datetime import date, datetime
-
-class FakeUser(BaseModel):
-    provider: Optional[str] = None
-    visa_center: Optional[str] = None
-    order_no: Optional[str] = None
-    social_account: Optional[str] = None
-    account: Optional[str] = None
-    password: Optional[str] = None
-    last_name: Optional[str] = None
-    first_name: Optional[str] = None
-    gender: Optional[str] = None
-    birthday: Optional[date] = None
-    email: Optional[str] = None
-    alias_email: Optional[str] = None
-    phone_country_code: Optional[str] = None
-    phone_no: Optional[str] = None
-    passport_no: Optional[str] = None
-    nationality: Optional[str] = None
-    passport_expiry_date: Optional[date] = None
-    address_line1: Optional[str] = None
-    address_line2: Optional[str] = None
-    state: Optional[str] = None
-    city: Optional[str] = None
-    postcode: Optional[str] = None
-    travel_date: Optional[date] = None
-    cover_letter: Optional[str] = None
-    passport_image_url: Optional[str] = None
-    selfie_image_url: Optional[str] = None
-    application_form_url: Optional[str] = None
-    priority: Optional[int] = None
-    expected_submit_start: Optional[date] = None
-    expected_submit_end: Optional[date] = None
-    rules: Optional[str] = None
-    status: Optional[int] = None
-    placeholder: Optional[int] = None
-    appointment_datetime: Optional[datetime] = None
-    appointment_letter_url: Optional[str] = None
-    pnr_number: Optional[str] = None
-    payment_link: Optional[str] = None
-    payment_help: Optional[int] = None
-    note: Optional[str] = None

+ 214 - 44
app/services/email_authorizations_service.py

@@ -8,7 +8,9 @@ import asyncio
 import re
 import re
 import time
 import time
 from datetime import datetime, timedelta, timezone
 from datetime import datetime, timedelta, timezone
+import email.policy
 from email.message import EmailMessage
 from email.message import EmailMessage
+from email.utils import formatdate, make_msgid
 from email.header import decode_header
 from email.header import decode_header
 from typing import List, Optional
 from typing import List, Optional
 
 
@@ -556,7 +558,6 @@ class EmailAuthorizationService:
             subject_keywords: str,
             subject_keywords: str,
             body_keywords: str
             body_keywords: str
     ):
     ):
-        # 整个逻辑比较复杂且都是 IO,直接打包扔进线程池
         def _worker():
         def _worker():
             subject_keys = [s.strip() for s in subject_keywords.split(",") if s.strip()]
             subject_keys = [s.strip() for s in subject_keywords.split(",") if s.strip()]
             body_keys = [s.strip() for s in body_keywords.split(",") if s.strip()]
             body_keys = [s.strip() for s in body_keywords.split(",") if s.strip()]
@@ -568,54 +569,110 @@ class EmailAuthorizationService:
                 mail.login(auth.email, auth.authorization_code)
                 mail.login(auth.email, auth.authorization_code)
                 mail.select("INBOX")
                 mail.select("INBOX")
                 
                 
+                # 1. 搜索目标邮件
                 target = recipient
                 target = recipient
-                # 注意:IMAP search 语法对引号很敏感,确保 target 没有特殊字符破坏命令
                 query = f'(HEADER To "{target}")' 
                 query = f'(HEADER To "{target}")' 
                 res, data = mail.uid("search", None, query)
                 res, data = mail.uid("search", None, query)
                 if res != "OK": return None
                 if res != "OK": return None
                 
                 
                 uids = data[0].split()
                 uids = data[0].split()
-                msgs = []
+                msgs_to_check = []
                 for uid in uids:
                 for uid in uids:
+                    # 使用 RFC822 获取完整内容
                     res, msg_data = mail.uid("fetch", uid, "(RFC822)")
                     res, msg_data = mail.uid("fetch", uid, "(RFC822)")
-                    if res != "OK": continue
-                    msg = email.message_from_bytes(msg_data[0][1])
-                    # 处理 Date 头可能缺失的情况
-                    date_str = msg.get("Date")
+                    if res != "OK" or not msg_data: continue
+                    
+                    # 临时解析用于排序和初步过滤
+                    raw_bytes = msg_data[0][1]
+                    tmp_msg = email.message_from_bytes(raw_bytes, policy=email.policy.default)
+                    
+                    date_str = tmp_msg.get("Date")
                     if date_str:
                     if date_str:
-                        date_ = email.utils.parsedate_to_datetime(date_str)
-                        msgs.append((date_, msg))
+                        try:
+                            date_dt = parsedate_to_datetime(date_str)
+                            msgs_to_check.append((date_dt, tmp_msg, raw_bytes))
+                        except:
+                            continue
                 
                 
-                msgs.sort(key=lambda x: x[0], reverse=True)
+                # 按时间降序排序(最新的优先)
+                msgs_to_check.sort(key=lambda x: x[0], reverse=True)
 
 
-                for _, msg in msgs:
-                    msg_from = msg.get("From", "")
+                for _, orig_msg, raw_bytes in msgs_to_check:
+                    # --- 过滤逻辑 ---
+                    msg_from = orig_msg.get("From", "")
                     if sender.lower() not in msg_from.lower(): continue
                     if sender.lower() not in msg_from.lower(): continue
                     
                     
-                    subject_raw = msg.get("Subject", "")
-                    subject = ""
-                    d = decode_header(subject_raw)[0]
-                    subject = d[0].decode(d[1] or "utf-8", errors="ignore") if isinstance(d[0], bytes) else str(d[0])
-                    
+                    subject = orig_msg.get("Subject", "")
                     if subject_keys and not any(k.lower() in subject.lower() for k in subject_keys): continue
                     if subject_keys and not any(k.lower() in subject.lower() for k in subject_keys): continue
                     
                     
-                    body = EmailAuthorizationService._extract_body(msg, True)
-                    if body_keys and not any(k.lower() in body.lower() for k in body_keys): continue
+                    body_content = EmailAuthorizationService._extract_body(orig_msg, True)
+                    if body_keys and not any(k.lower() in body_content.lower() for k in body_keys): continue
 
 
-                    # 准备转发
-                    # 注意:直接修改 Header 转发可能会破坏 DKIM 签名,更好的方式是作为附件转发
-                    # 但这里保持原逻辑
-                    del msg['From']
-                    del msg['To']
-                    del msg['Subject']
+                    # --- 匹配成功:开始构造转发邮件 ---
+                    
+                    # 1. 提取原始信息用于视觉转发头
+                    orig_from = orig_msg.get("From", "Unknown")
+                    orig_date = orig_msg.get("Date", "Unknown")
+                    orig_subject = orig_msg.get("Subject", "No Subject")
+                    orig_to = orig_msg.get("To", "Unknown")
+                    orig_msg_id = orig_msg.get("Message-ID")
+
+                    fwd_info = (
+                        f"\n\n---------- Forwarded message ----------\n"
+                        f"From: {orig_from}\n"
+                        f"Date: {orig_date}\n"
+                        f"Subject: {orig_subject}\n"
+                        f"To: {orig_to}\n\n"
+                    )
+
+                    # 2. 构造新的邮件对象 (重新基于原始字节解析,确保附件完整)
+                    msg = email.message_from_bytes(raw_bytes, policy=email.policy.default)
+
+                    # 3. 清理并重置 Header
+                    headers_to_clean = ['From', 'To', 'Cc', 'Bcc', 'Subject', 'Date', 'Message-ID', 'In-Reply-To', 'References']
+                    for h in headers_to_clean:
+                        del msg[h]
+                    
                     msg['From'] = auth.email
                     msg['From'] = auth.email
                     msg['To'] = forward_to
                     msg['To'] = forward_to
-                    msg['Subject'] = f"FWD: {subject}"
-                    
-                    # 调用同步的 send 方法
+                    msg['Subject'] = f"Fwd: {orig_subject}"
+                    msg['Date'] = formatdate(localtime=True)
+                    msg['Message-ID'] = make_msgid(domain=auth.email.split('@')[-1])
+
+                    # 4. 【核心】建立上下文关联 (Threading)
+                    if orig_msg_id:
+                        msg['In-Reply-To'] = orig_msg_id
+                        msg['References'] = orig_msg_id
+
+                    # 5. 【核心】注入视觉转发头 (Visual Prepend)
+                    try:
+                        if msg.is_multipart():
+                            # 遍历部分,找到主要正文并插入
+                            for part in msg.walk():
+                                ctype = part.get_content_type()
+                                if ctype == "text/plain":
+                                    part.set_content(fwd_info + part.get_content())
+                                    break
+                                elif ctype == "text/html":
+                                    html_fwd = fwd_info.replace("\n", "<br>")
+                                    part.set_content(f"<div>{html_fwd}</div>" + part.get_content(), subtype="html")
+                                    break
+                        else:
+                            msg.set_content(fwd_info + msg.get_content())
+                    except Exception as e:
+                        logger.warning(f"Prepend visual header failed: {e}")
+
+                    # 6. 发送邮件
                     EmailAuthorizationService.send_email_smtp(auth, msg)
                     EmailAuthorizationService.send_email_smtp(auth, msg)
                     
                     
-                    return f"邮件 '{subject}' 已成功转发至: {forward_to}"
+                    # 7. 同步发件记录 (IMAP Sent)
+                    EmailAuthorizationService._append_to_sent(auth, msg)
+                    
+                    return f"邮件 '{orig_subject}' 已成功关联转发至: {forward_to}"
+                
+                return None
+            except Exception as e:
+                logger.error(f"Forward matching email error: {e}")
                 return None
                 return None
             finally:
             finally:
                 try:
                 try:
@@ -727,21 +784,66 @@ class EmailAuthorizationService:
                     # 如果 data[0] 只是 bytes (例如 b')'),说明没拿到邮件体
                     # 如果 data[0] 只是 bytes (例如 b')'),说明没拿到邮件体
                     return f"邮件数据格式异常,无法解析: {str(data)}"
                     return f"邮件数据格式异常,无法解析: {str(data)}"
 
 
-                msg = email.message_from_bytes(raw_email_bytes)
+                # 使用 default policy 解析,方便后续修改
+                orig_msg = email.message_from_bytes(data[0][1], policy=email.policy.default)
+
+                # --- 1. 提取原始邮件信息用于构造转发头 ---
+                orig_from = orig_msg.get("From", "Unknown")
+                orig_date = orig_msg.get("Date", "Unknown")
+                orig_subject = orig_msg.get("Subject", "No Subject")
+                orig_to = orig_msg.get("To", "Unknown")
+                orig_msg_id = orig_msg.get("Message-ID")
+
+                # --- 2. 构造视觉上的“转发信息栏” ---
+                fwd_header_text = (
+                    f"\n\n---------- Forwarded message ----------\n"
+                    f"From: {orig_from}\n"
+                    f"Date: {orig_date}\n"
+                    f"Subject: {orig_subject}\n"
+                    f"To: {orig_to}\n\n"
+                )
 
 
-                # 3. 处理转发逻辑
-                # 使用数据库中查到的标题,确保标题准确
-                subject = target_subject 
+                # --- 3. 构造新的邮件对象 ---
+                # 为了保持上下文关联,我们克隆或重新构造,并设置 Threading Headers
+                msg = email.message_from_bytes(data[0][1], policy=email.policy.default)
 
 
-                # 修改 Header 进行转发
-                del msg['From']
-                del msg['To']
-                del msg['Cc']
-                del msg['Subject']
+               # 清除旧头
+                for h in ['From', 'To', 'Cc', 'Bcc', 'Subject', 'Date', 'Message-ID', 'In-Reply-To', 'References']:
+                    del msg[h]
                 
                 
-                msg['From'] = auth.email  # 发件人覆写为当前授权账号
+                msg['From'] = auth.email
                 msg['To'] = forward_to
                 msg['To'] = forward_to
-                msg['Subject'] = f"FWD: {subject}"
+                msg['Subject'] = f"Fwd: {target_subject}"
+                msg['Date'] = formatdate(localtime=True)
+                msg['Message-ID'] = make_msgid(domain=auth.email.split('@')[-1])
+                
+                # --- 4. 关键:建立线索关联 (Threading) ---
+                if orig_msg_id:
+                    # 这两个头告诉 Gmail 这封信是原邮件的后续
+                    msg['In-Reply-To'] = orig_msg_id
+                    msg['References'] = orig_msg_id
+                    
+                # --- 5. 修改正文,注入转发视觉头 ---
+                # 处理 Multipart 或简单邮件,将 fwd_header_text 插入到正文最前面
+                try:
+                    if msg.is_multipart():
+                        # 找到第一个文本部分并修改
+                        for part in msg.walk():
+                            if part.get_content_type() == "text/plain":
+                                content = part.get_content()
+                                part.set_content(fwd_header_text + content)
+                                break
+                            elif part.get_content_type() == "text/html":
+                                # HTML 转发头稍微复杂点,这里简单处理
+                                content = part.get_content()
+                                html_fwd = fwd_header_text.replace("\n", "<br>")
+                                part.set_content(f"<div>{html_fwd}</div>" + content, subtype="html")
+                                break
+                    else:
+                        content = msg.get_content()
+                        msg.set_content(fwd_header_text + content)
+                except Exception as e:
+                    logger.warning(f"Failed to prepend forward header: {e}")
                 
                 
                 # 4. 发送邮件 (SMTP)
                 # 4. 发送邮件 (SMTP)
                 EmailAuthorizationService.send_email_smtp(auth, msg)
                 EmailAuthorizationService.send_email_smtp(auth, msg)
@@ -759,6 +861,61 @@ class EmailAuthorizationService:
 
 
         # 在线程池中运行耗时 IO 操作
         # 在线程池中运行耗时 IO 操作
         return await run_in_threadpool(_worker)
         return await run_in_threadpool(_worker)
+    
+    @staticmethod
+    def _append_to_sent(auth, msg: EmailMessage):
+        """
+        同步发件记录到 IMAP Sent 文件夹
+        """
+        imap = None
+        try:
+            # 确保消息包含必要的指纹,否则同步后 Gmail 搜索不到
+            if 'Date' not in msg:
+                msg["Date"] = formatdate(localtime=True)
+            if 'Message-ID' not in msg:
+                msg["Message-ID"] = make_msgid(domain=auth.email.split('@')[-1])
+
+            imap = EmailAuthorizationService._connect_imap_with_proxy(
+                auth.imap_server, auth.imap_port,
+                auth.proxy_host, auth.proxy_port,
+                auth.proxy_username, auth.proxy_password,
+            )
+            imap.login(auth.email, auth.authorization_code)
+
+            # --- 自动探测已发送文件夹 (兼容 Gmail/Outlook/域名邮) ---
+            sent_folder = None
+            typ, data = imap.list()
+            if typ == "OK":
+                for entry in data:
+                    line = entry.decode()
+                    # 寻找包含 \Sent 属性的系统文件夹
+                    if '\\Sent' in line:
+                        # 兼容各种分隔符,提取最后一个引号内的内容
+                        parts = re.findall(r'"([^"]+)"', line)
+                        if parts:
+                            sent_folder = f'"{parts[-1]}"' # 强制带引号防止空格导致 BAD
+                            break
+            
+            # 兜底逻辑
+            if not sent_folder:
+                sent_folder = '"[Gmail]/Sent Mail"' if "gmail" in auth.email.lower() else '"Sent"'
+
+            # 执行写入 (使用 \\Seen 标记为已读)
+            # imap.append 的参数顺序: 文件夹, 标志, 时间, 内容
+            imap.append(
+                sent_folder,
+                '(\\Seen)',
+                imaplib.Time2Internaldate(time.time()),
+                msg.as_bytes()
+            )
+            logger.info(f"Successfully synced to folder: {sent_folder}")
+
+        except Exception as e:
+            logger.error(f"Append sent mail failed: {str(e)}")
+        finally:
+            if imap:
+                try: imap.logout()
+                except: pass
 
 
     @staticmethod
     @staticmethod
     async def send_email(
     async def send_email(
@@ -774,13 +931,21 @@ class EmailAuthorizationService:
             msg["To"] = send_to
             msg["To"] = send_to
             msg["Subject"] = subject
             msg["Subject"] = subject
             
             
+            msg["Date"] = formatdate(localtime=True)
+            msg["Message-ID"] = make_msgid(domain=auth.email.split('@')[-1])
+            msg["MIME-Version"] = "1.0"
+            msg["X-Mailer"] = "Python-Client-v1.0"
+            
             if content_type.lower() == "html":
             if content_type.lower() == "html":
                 msg.set_content("") # 占位
                 msg.set_content("") # 占位
                 msg.add_alternative(content, subtype="html")
                 msg.add_alternative(content, subtype="html")
             else:
             else:
                 msg.set_content(content)
                 msg.set_content(content)
 
 
+            # 2. 执行发送
+            logger.info(f"[DEBUG] 准备发送邮件: ID={msg['Message-ID']}")
             EmailAuthorizationService.send_email_smtp(auth, msg)
             EmailAuthorizationService.send_email_smtp(auth, msg)
+            
             return f"邮件 '{subject}' 成功发送至: {send_to}"
             return f"邮件 '{subject}' 成功发送至: {send_to}"
 
 
         return await run_in_threadpool(_worker)
         return await run_in_threadpool(_worker)
@@ -830,10 +995,15 @@ class EmailAuthorizationService:
         )
         )
         try:
         try:
             mail.login(auth.email, auth.authorization_code)
             mail.login(auth.email, auth.authorization_code)
-            if bcc_list:
-                mail.send_message(msg, to_addrs=bcc_list)
-            else:
-                mail.send_message(msg)
+            recipients = bcc_list if bcc_list else [msg["To"]]
+            mail.send_message(
+                msg,
+                from_addr=auth.email,
+                to_addrs=recipients
+            )
+            
+            logger.info(f"[DEBUG] 开始同步到已发送文件夹...")
+            EmailAuthorizationService._append_to_sent(auth, msg)
         finally:
         finally:
             mail.quit()
             mail.quit()
 
 

+ 127 - 235
app/services/fake_service.py

@@ -1,9 +1,20 @@
+import json
 import random
 import random
+import aiohttp
+import asyncio
+import string
+import uuid
+
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import select, delete
 from app.core.biz_exception import NotFoundError, PermissionDeniedError, BizLogicError
 from app.core.biz_exception import NotFoundError, PermissionDeniedError, BizLogicError
-from typing import List
+from app.models.user import VasUser
+from app.models.product import VasProduct
+from app.models.schema import VasSchema
+from app.schemas.order import VasOrderCreate
+from typing import List, Optional
 from datetime import date, datetime, timedelta
 from datetime import date, datetime, timedelta
-from app.schemas.fake import FakeUser
-
+from app.services.order_service import OrderService
 # ----------------------------------------
 # ----------------------------------------
 # 邮箱域名
 # 邮箱域名
 # ----------------------------------------
 # ----------------------------------------
@@ -16,243 +27,124 @@ DOMAINS = [
     "teamymail.cfd"
     "teamymail.cfd"
 ]
 ]
 
 
-def get_email_prefix(email: str) -> str:
-    if not email or "@" not in email:
-        return ""
-    return email.split("@", 1)[0]
-
-def generate_stable_email(base: str) -> str:
-    if not base or not base.strip():
-        base = "user"
-    index = abs(hash(base)) % len(DOMAINS)
-    return f"{base}@{DOMAINS[index]}"
-
-# ----------------------------------------
-# 国籍对应名字和护照格式
-# 全 ASCII
-# ----------------------------------------
-NATIONALITY_DATA = {
-    "China": {
-        "male_names": ["Wei", "Jian", "Liang", "Hao", "Jun"],
-        "female_names": ["Mei", "Li", "Fang", "Xia", "Lan"],
-        "surnames": ["Zhang", "Wang", "Li", "Liu", "Chen"],
-        "passport_pattern": "E?########"
-    },
-    "India": {
-        "male_names": ["Arjun", "Rohan", "Raj", "Vikram", "Siddharth"],
-        "female_names": ["Ananya", "Priya", "Aisha", "Neha", "Kavya"],
-        "surnames": ["Sharma", "Patel", "Singh", "Kumar", "Gupta"],
-        "passport_pattern": "?#######"
-    },
-    # "United States": {
-    #     "male_names": ["James", "John", "Robert", "Michael", "William"],
-    #     "female_names": ["Mary", "Patricia", "Linda", "Elizabeth", "Jennifer"],
-    #     "surnames": ["Smith", "Johnson", "Williams", "Brown", "Jones"],
-    #     "passport_pattern": "#########"
-    # },
-    # "United Kingdom": {
-    #     "male_names": ["Oliver", "George", "Harry", "Jack", "Jacob"],
-    #     "female_names": ["Olivia", "Amelia", "Isla", "Ava", "Emily"],
-    #     "surnames": ["Smith", "Jones", "Taylor", "Brown", "Williams"],
-    #     "passport_pattern": "#########"
-    # },
-    # "Ireland": {
-    #     "male_names": ["Conor", "Sean", "Patrick", "Liam", "Finn"],
-    #     "female_names": ["Aoife", "Saoirse", "Emily", "Grace", "Ciara"],
-    #     "surnames": ["Murphy", "Kelly", "OBrien", "Walsh", "Ryan"],
-    #     "passport_pattern": "P########"
-    # },
-    # "Germany": {
-    #     "male_names": ["Max", "Lukas", "Felix", "Leon", "Paul"],
-    #     "female_names": ["Anna", "Emma", "Sophia", "Marie", "Laura"],
-    #     "surnames": ["Mueller", "Schmidt", "Schneider", "Fischer", "Weber"],
-    #     "passport_pattern": "C########"
-    # },
-    # "France": {
-    #     "male_names": ["Louis", "Gabriel", "Raphael", "Arthur", "Jules"],
-    #     "female_names": ["Emma", "Louise", "Alice", "Chloe", "Lina"],
-    #     "surnames": ["Martin", "Bernard", "Thomas", "Robert", "Richard"],
-    #     "passport_pattern": "########"
-    # },
-    # "Japan": {
-    #     "male_names": ["Hiroshi", "Takumi", "Kenta", "Yuki", "Ryo"],
-    #     "female_names": ["Yui", "Sakura", "Mao", "Aya", "Hana"],
-    #     "surnames": ["Sato", "Suzuki", "Takahashi", "Tanaka", "Watanabe"],
-    #     "passport_pattern": "TR#######"
-    # }
-}
-
-# ----------------------------------------
-# 居住国家地址/电话/邮编(全 ASCII)
-# ----------------------------------------
-RESIDENCE_DATA = {
-    "China": {
-        "cities": ["Beijing", "Shanghai", "Shenzhen", "Guangzhou", "Chengdu"],
-        "streets": ["Heping Road", "Jiefang Road", "Renmin Street", "Zhongshan Avenue", "Fuxing Street"],
-        "postcode_prefix": "1",
-        "phone_code": "+86",
-        "phone_length": 11
-    },
-    "India": {
-        "cities": ["Delhi", "Mumbai", "Bangalore", "Kolkata", "Chennai"],
-        "streets": ["MG Road", "Connaught Place", "Brigade Road", "Park Street", "Anna Salai"],
-        "postcode_prefix": "5",
-        "phone_code": "+91",
-        "phone_length": 10
-    },
-    "United Kingdom": {
-        "cities": ["London", "Manchester", "Birmingham", "Leeds", "Glasgow"],
-        "streets": ["High Street", "Station Road", "Main Street", "Church Road", "London Road"],
-        "postcode_prefix": "SW",
-        "phone_code": "+44",
-        "phone_length": 10
-    },
-    "Ireland": {
-        "cities": ["Dublin", "Cork", "Galway", "Limerick", "Waterford"],
-        "streets": ["OConnell Street", "Patrick Street", "Main Street", "High Street", "Bridge Street"],
-        "postcode_prefix": "D",
-        "phone_code": "+353",
-        "phone_length": 9
-    }
-}
-
-# ----------------------------------------
-# 辅助函数
-# ----------------------------------------
-def generate_passport(pattern: str) -> str:
-    s = ""
-    for c in pattern:
-        if c == "#":
-            s += str(random.randint(0, 9))
-        elif c == "?":
-            s += random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-        else:
-            s += c
-    return s
-
-def generate_name(nationality: str, gender: str):
-    data = NATIONALITY_DATA[nationality]
-    if gender == "Male":
-        first = random.choice(data["male_names"])
-    else:
-        first = random.choice(data["female_names"])
-    last = random.choice(data["surnames"])
-    return first, last
-
-def generate_address(residence_country: str):
-    data = RESIDENCE_DATA.get(residence_country)
-    street = random.choice(data["streets"])
-    city = random.choice(data["cities"])
-    postcode = data["postcode_prefix"] + "".join([str(random.randint(0, 9)) for _ in range(5)])
-    return f"{random.randint(1, 200)} {street}", city, postcode
-
-# def generate_phone(residence_country: str):
-#     data = RESIDENCE_DATA.get(residence_country)
-#     num = "".join([str(random.randint(0,9)) for _ in range(data["phone_length"])])
-#     return data["phone_code"], num
-
-def generate_phone(country: str):
-    if country == "China":
-        prefix = "86"
-        first_digit = "1"
-        length = 11
-    elif country == "India":
-        prefix = "91"
-        first_digit = random.choice(["7", "8", "9"])
-        length = 10
-    elif country == "Ireland":
-        prefix = "353"
-        first_digit = "8"
-        length = 9
-    elif country == "United Kingdom":
-        prefix = "44"
-        first_digit = "7"
-        length = 10
-    elif country == "United States":
-        prefix = "1"
-        first_digit = str(random.randint(2,9))
-        length = 10
-    elif country == "Germany":
-        prefix = "49"
-        first_digit = str(random.randint(1,9))
-        length = 10
-    else:
-        prefix = "+00"
-        first_digit = "1"
-        length = 10
+class FakeService:
     
     
-    # 生成国内号码(ASCII数字)
-    number = first_digit + "".join(str(random.randint(0,9)) for _ in range(length-1))
-    return prefix, '0' + number
-
-# ----------------------------------------
-# 主函数
-# ----------------------------------------
-def generate_fake_users(num: int, living_country: str = "Ireland") -> List[FakeUser]:
-    users = []
-    if living_country not in RESIDENCE_DATA.keys():
-        raise BizLogicError(f'Living country only have {RESIDENCE_DATA.keys()}')
-    for _ in range(num):
-        gender = random.choice(["Male","Female"])
-        nationality = random.choice(list(NATIONALITY_DATA.keys()))
-        first_name, last_name = generate_name(nationality, gender)
-        passport_no = generate_passport(NATIONALITY_DATA[nationality]["passport_pattern"])
-
-        address_line1, city, postcode = generate_address(living_country)
-        phone_country_code, phone_no = generate_phone(living_country)
-
-        fake_email = f"{first_name.lower()}.{last_name.lower()}@example.com"
-        alias_email = generate_stable_email(f"{first_name.lower()}.{last_name.lower()}")
+    RANDOM_USER_API = "https://randomuser.me/api/?nat=ie"
+
+    @staticmethod
+    def random_passport():
+        """生成简单护照号"""
+        prefix = random.choice(["P", "L", "X"])
+        digits = "".join(random.choices(string.digits, k=8))
+        return prefix + digits
+
+    @staticmethod
+    def random_passport_expiry():
+        """护照有效期 3-10 年"""
+        today = datetime.today()
+        future = today + timedelta(days=random.randint(3*365, 10*365))
+        return future.strftime("%Y-%m-%d")
+
+    @staticmethod
+    def random_expected_dates():
+        """预约时间范围"""
+        start = datetime.today() + timedelta(days=4)
+        end = start + timedelta(days=random.randint(7, 60))
+        return start.strftime("%Y-%m-%d"), end.strftime("%Y-%m-%d")
+    
+    @staticmethod
+    def random_gmail(first, last):
+        """
+        生成随机 Gmail
+        """
+        first = first.lower()
+        last = last.lower()
+
+        patterns = [
+            f"{first}{random.randint(10,99)}",
+            f"{first}.{last}{random.randint(10,999)}",
+            f"{first[0]}{last}{random.randint(100,999)}"
+            f"{first}{uuid.uuid4().hex[:3]}",
+            f"{first}.{uuid.uuid4().hex[:4]}",
+            f"{first[0]}{uuid.uuid4().hex[:5]}"
+            
+        ]
+
+        name = random.choice(patterns)
+
+        return f"{name}@gmail.com"
+
+    @staticmethod
+    async def fetch_irish_users(num: int):
+
+        url = f"https://randomuser.me/api/?results={num}&nat=ie"
+
+        async with aiohttp.ClientSession() as session:
+            async with session.get(url) as resp:
+                data = await resp.json()
+
+        return data["results"]
+    
+    @staticmethod
+    async def generate_visametric_applicants(num: int):
 
 
-        travel_date = date.today() + timedelta(days=random.randint(30,90))
-        passport_expiry_date = date.today() + timedelta(days=random.randint(365,3650))
-        birthday = date.today() - timedelta(days=random.randint(18*365,50*365))
+        users = await FakeService.fetch_irish_users(num)
 
 
-        user = FakeUser(
-            provider="fake",
-            visa_center="fake",
-            order_no=f"{random.randint(10000000,99999999)}",
-            social_account=f"fake-{first_name.lower()}{random.randint(1,99)}",
-            account=fake_email,
-            password="Password123!",
+        results = []
 
 
-            last_name=last_name,
-            first_name=first_name,
-            gender=gender,
-            birthday=birthday,
-            nationality=nationality,
-            passport_no=passport_no,
-            passport_expiry_date=passport_expiry_date,
-            email=fake_email,
-            alias_email=alias_email,
+        for u in users:
 
 
-            address_line1=address_line1,
-            address_line2="",
-            city=city,
-            state="",
-            postcode=postcode,
-            phone_country_code=phone_country_code,
-            phone_no=phone_no,
+            first = u["name"]["first"]
+            last = u["name"]["last"]
+            start_date, end_date = FakeService.random_expected_dates()
 
 
-            travel_date=travel_date,
-            cover_letter="This is a test cover letter.",
-            passport_image_url="https://placekitten.com/400/400",
-            selfie_image_url="https://placekitten.com/401/401",
-            application_form_url="https://example.com/form",
+            item = {
+                "first_name": u["name"]["first"],
+                "last_name": u["name"]["last"],
+                "birthday": u["dob"]["date"][:10],
+                "passport_no": FakeService.random_passport(),
+                "passport_expiry_date": FakeService.random_passport_expiry(),
+                "email": FakeService.random_gmail(first, last),
+                "phone_country_code": "353",
+                "phone_no": u["cell"].replace(" ", "").replace("-", ""),
+                "expected_start_date": start_date,
+                "expected_end_date": end_date,
+                "social_media_account": f"fake-{u['login']['username']}"
+            }
 
 
-            priority=random.randint(1,5),
-            expected_submit_start='2000-01-01',
-            expected_submit_end='2100-01-01',
-            rules="",
-            status=0,
-            placeholder=1,
-            appointment_datetime=datetime.now() + timedelta(days=random.randint(20,200)),
-            appointment_letter_url="https://example.com/appointment",
-            pnr_number="",
-            payment_link="",
-            payment_help=1,
-            note="This is a test note."
-        )
-        users.append(user)
+            results.append(item)
 
 
-    return users
+        return results
+    
+    @staticmethod
+    async def generate_orders(db: AsyncSession, num: int, product_id: int, auth_user: VasUser):
+        stmt = select(VasProduct).where(VasProduct.id == product_id)
+        product_obj = (await db.execute(stmt)).scalar_one_or_none()
+        if not product_obj:
+            raise NotFoundError("Product not exist")
+        
+        schema_id = product_obj.schema_id
+        stmt = select(VasSchema).where(VasSchema.id == schema_id)
+        schema_obj = (await db.execute(stmt)).scalar_one_or_none()
+        if not schema_obj:
+            raise NotFoundError("Schema not exist")
+        schmea_json = schema_obj.schema_json
+        
+        applicants = await FakeService.generate_visametric_applicants(num=num)
+        
+        orders = []
+        print(json.dumps(applicants, ensure_ascii=False, indent=2))
+        for app in applicants:
+            order_data = VasOrderCreate(
+                product_id=product_id,
+                user_inputs=app
+            )
+
+            obj = await OrderService.create_by_admin(
+                db=db,
+                data=order_data,
+                product=product_obj,
+                auth_user=auth_user
+            )
+            orders.append(obj)
+        return orders

+ 1 - 2
app/services/order_service.py

@@ -86,8 +86,7 @@ class OrderService:
         db: AsyncSession,
         db: AsyncSession,
         data: VasOrderCreate,
         data: VasOrderCreate,
         product: VasProduct,
         product: VasProduct,
-        auth_user: VasUser,
-        redis_client: Redis,
+        auth_user: VasUser
     ) -> VasOrder:
     ) -> VasOrder:
         order_id = f"ORD-{datetime.utcnow():%Y%m%d%H%M%S}-{uuid.uuid4().hex[:8]}"
         order_id = f"ORD-{datetime.utcnow():%Y%m%d%H%M%S}-{uuid.uuid4().hex[:8]}"
         order = VasOrder(
         order = VasOrder(

+ 10 - 2
app/services/ticket_service.py

@@ -2,7 +2,7 @@ from datetime import datetime
 from typing import List, Optional
 from typing import List, Optional
 
 
 from redis.asyncio import Redis
 from redis.asyncio import Redis
-from sqlalchemy import select
+from sqlalchemy import select, case
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.ext.asyncio import AsyncSession
 
 
 from app.utils.search import apply_keyword_search_stmt
 from app.utils.search import apply_keyword_search_stmt
@@ -250,6 +250,7 @@ class TicketService:
     ):
     ):
         stmt = select(VasTicket)
         stmt = select(VasTicket)
 
 
+        # 关键词搜索
         stmt = apply_keyword_search_stmt(
         stmt = apply_keyword_search_stmt(
             stmt=stmt,
             stmt=stmt,
             model=VasTicket,
             model=VasTicket,
@@ -257,6 +258,13 @@ class TicketService:
             fields=["order_id", "user_id", "reason", "admin_comment"],
             fields=["order_id", "user_id", "reason", "admin_comment"],
         )
         )
 
 
-        stmt = stmt.order_by(VasTicket.id.desc())
+        # 排序:未解决优先 ('pending','info_required') -> 0, 已解决 ('resolved','rejected') -> 1
+        stmt = stmt.order_by(
+            case(
+                (VasTicket.status.in_(["pending", "info_required"]), 0),
+                else_=1
+            ),
+            VasTicket.id.desc()
+        )
 
 
         return await paginate(db, stmt, page, size)
         return await paginate(db, stmt, page, size)