Bladeren bron

feat: update

jerry 2 maanden geleden
bovenliggende
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.short_url import ShortUrlCreate, ShortUrlOut
 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.user import VasUserCreate, VasUserUpdate, VasUserSetProfiles, VasUserOut
 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.seaweedfs_service import SeaweedFSService
 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.user_service import UserService
 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)
     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="生成几个数据"),
-    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])
 async def slots_latest_get(
@@ -1065,8 +1066,7 @@ async def vas_order_create(
 async def vas_order_create_by_admin(
     payload: VasOrderCreate,
     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)
     # ① 获取产品绑定的 schema
@@ -1076,7 +1076,7 @@ async def vas_order_create_by_admin(
         schema_json=schema.schema_json,
         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)
 
 @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 time
 from datetime import datetime, timedelta, timezone
+import email.policy
 from email.message import EmailMessage
+from email.utils import formatdate, make_msgid
 from email.header import decode_header
 from typing import List, Optional
 
@@ -556,7 +558,6 @@ class EmailAuthorizationService:
             subject_keywords: str,
             body_keywords: str
     ):
-        # 整个逻辑比较复杂且都是 IO,直接打包扔进线程池
         def _worker():
             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()]
@@ -568,54 +569,110 @@ class EmailAuthorizationService:
                 mail.login(auth.email, auth.authorization_code)
                 mail.select("INBOX")
                 
+                # 1. 搜索目标邮件
                 target = recipient
-                # 注意:IMAP search 语法对引号很敏感,确保 target 没有特殊字符破坏命令
                 query = f'(HEADER To "{target}")' 
                 res, data = mail.uid("search", None, query)
                 if res != "OK": return None
                 
                 uids = data[0].split()
-                msgs = []
+                msgs_to_check = []
                 for uid in uids:
+                    # 使用 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:
-                        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
                     
-                    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
                     
-                    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['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)
                     
-                    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
             finally:
                 try:
@@ -727,21 +784,66 @@ class EmailAuthorizationService:
                     # 如果 data[0] 只是 bytes (例如 b')'),说明没拿到邮件体
                     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['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)
                 EmailAuthorizationService.send_email_smtp(auth, msg)
@@ -759,6 +861,61 @@ class EmailAuthorizationService:
 
         # 在线程池中运行耗时 IO 操作
         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
     async def send_email(
@@ -774,13 +931,21 @@ class EmailAuthorizationService:
             msg["To"] = send_to
             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":
                 msg.set_content("") # 占位
                 msg.add_alternative(content, subtype="html")
             else:
                 msg.set_content(content)
 
+            # 2. 执行发送
+            logger.info(f"[DEBUG] 准备发送邮件: ID={msg['Message-ID']}")
             EmailAuthorizationService.send_email_smtp(auth, msg)
+            
             return f"邮件 '{subject}' 成功发送至: {send_to}"
 
         return await run_in_threadpool(_worker)
@@ -830,10 +995,15 @@ class EmailAuthorizationService:
         )
         try:
             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:
             mail.quit()
 

+ 127 - 235
app/services/fake_service.py

@@ -1,9 +1,20 @@
+import json
 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 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 app.schemas.fake import FakeUser
-
+from app.services.order_service import OrderService
 # ----------------------------------------
 # 邮箱域名
 # ----------------------------------------
@@ -16,243 +27,124 @@ DOMAINS = [
     "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,
         data: VasOrderCreate,
         product: VasProduct,
-        auth_user: VasUser,
-        redis_client: Redis,
+        auth_user: VasUser
     ) -> VasOrder:
         order_id = f"ORD-{datetime.utcnow():%Y%m%d%H%M%S}-{uuid.uuid4().hex[:8]}"
         order = VasOrder(

+ 10 - 2
app/services/ticket_service.py

@@ -2,7 +2,7 @@ from datetime import datetime
 from typing import List, Optional
 
 from redis.asyncio import Redis
-from sqlalchemy import select
+from sqlalchemy import select, case
 from sqlalchemy.ext.asyncio import AsyncSession
 
 from app.utils.search import apply_keyword_search_stmt
@@ -250,6 +250,7 @@ class TicketService:
     ):
         stmt = select(VasTicket)
 
+        # 关键词搜索
         stmt = apply_keyword_search_stmt(
             stmt=stmt,
             model=VasTicket,
@@ -257,6 +258,13 @@ class TicketService:
             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)