jerry 3 месяцев назад
Родитель
Сommit
cd2ac066ca
4 измененных файлов с 266 добавлено и 134 удалено
  1. 6 2
      app/api/router.py
  2. 120 130
      app/services/task_handlers.py
  3. 3 2
      app/services/vas_task_service.py
  4. 137 0
      app/tasks/notification_task.py

+ 6 - 2
app/api/router.py

@@ -1300,8 +1300,12 @@ async def vas_task_return_to_queue(task_id:int, db: AsyncSession = Depends(get_d
     return success(data=obj)
     return success(data=obj)
 
 
 @admin_required_router.post("/vas/task/manual_confirm", summary="设置任务完成", tags=["Visafly签证系统"], response_model=ApiResponse[VasTaskOut])
 @admin_required_router.post("/vas/task/manual_confirm", summary="设置任务完成", tags=["Visafly签证系统"], response_model=ApiResponse[VasTaskOut])
-async def vas_task_manual_confirm(task_id:int, db: AsyncSession = Depends(get_db)):
-    obj = await VasTaskService.manual_confirm(db, task_id)
+async def vas_task_manual_confirm(
+    task_id:int,
+    db: AsyncSession = Depends(get_db),
+    redis_client: Redis = Depends(get_redis_client)
+):
+    obj = await VasTaskService.manual_confirm(db, task_id, redis_client)
     return success(data=obj)
     return success(data=obj)
 
 
 @admin_required_router.get("/vas/task/pop", summary="任务出队(pop)", tags=["Visafly签证系统"], response_model=ApiResponse[VasTaskOut])
 @admin_required_router.get("/vas/task/pop", summary="任务出队(pop)", tags=["Visafly签证系统"], response_model=ApiResponse[VasTaskOut])

+ 120 - 130
app/services/task_handlers.py

@@ -5,17 +5,18 @@ from datetime import datetime
 from typing import Callable, Awaitable, Dict, Any
 from typing import Callable, Awaitable, Dict, Any
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy import select
 from sqlalchemy import select
+from redis.asyncio import Redis
+
 from app.models.user import VasUser
 from app.models.user import VasUser
 from app.models.vas_task import VasTask
 from app.models.vas_task import VasTask
 from app.models.order import VasOrder
 from app.models.order import VasOrder
-
-# 导入具体的业务服务 (用于发邮件等)
+from app.services.notification_service import NotificationService
 from app.services.email_authorizations_service import EmailAuthorizationService
 from app.services.email_authorizations_service import EmailAuthorizationService
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
-# 定义处理器函数的类型签名
-HandlerFunc = Callable[[AsyncSession, VasTask, VasOrder], Awaitable[None]]
+# 1. 修改 HandlerFunc 类型签名,增加 redis_client: Redis
+HandlerFunc = Callable[[AsyncSession, Redis, VasTask, VasOrder], Awaitable[None]]
 
 
 class TaskHandlerRegistry:
 class TaskHandlerRegistry:
     """
     """
@@ -26,14 +27,6 @@ class TaskHandlerRegistry:
         self._handlers: Dict[str, HandlerFunc] = {}
         self._handlers: Dict[str, HandlerFunc] = {}
 
 
     def register(self, *routing_keys: str):
     def register(self, *routing_keys: str):
-        """
-        装饰器:将一个或多个 routing_key 注册到同一个处理函数
-        
-        用法:
-        @task_processor.register("key1", "key2")
-        async def my_handler(...):
-            pass
-        """
         def decorator(func: HandlerFunc):
         def decorator(func: HandlerFunc):
             for key in routing_keys:
             for key in routing_keys:
                 if key in self._handlers:
                 if key in self._handlers:
@@ -42,47 +35,38 @@ class TaskHandlerRegistry:
             return func
             return func
         return decorator
         return decorator
 
 
-    async def execute(self, routing_key: str, db: AsyncSession, task: VasTask, order: VasOrder):
+    # 2. 修改 execute 方法,接收 redis_client
+    async def execute(self, routing_key: str, db: AsyncSession, redis_client: Redis, task: VasTask, order: VasOrder):
         """
         """
         根据 routing_key 查找并执行对应的处理函数
         根据 routing_key 查找并执行对应的处理函数
         """
         """
         handler = self._handlers.get(routing_key)
         handler = self._handlers.get(routing_key)
         
         
         if not handler:
         if not handler:
-            # 如果没有注册处理器,通常直接跳过即可
             logger.info(f"ℹ️ [TaskHandler] No handler found for routing_key: {routing_key}, skipping.")
             logger.info(f"ℹ️ [TaskHandler] No handler found for routing_key: {routing_key}, skipping.")
             return
             return
 
 
         try:
         try:
             logger.info(f"▶️ [TaskHandler] Executing handler '{handler.__name__}' for task {task.id} (key: {routing_key})")
             logger.info(f"▶️ [TaskHandler] Executing handler '{handler.__name__}' for task {task.id} (key: {routing_key})")
             
             
-            # 执行具体的业务逻辑
-            await handler(db, task, order)
+            # 传递 redis_client 给具体处理函数
+            await handler(db, redis_client, task, order)
             
             
             logger.info(f"✅ [TaskHandler] Handler '{handler.__name__}' completed successfully.")
             logger.info(f"✅ [TaskHandler] Handler '{handler.__name__}' completed successfully.")
             
             
         except Exception as e:
         except Exception as e:
-            # 捕获异常,防止阻断主流程 (manual_confirm) 的事务提交
-            # 也可以选择在这里将错误写入 task.meta
             logger.error(f"❌ [TaskHandler] Error executing handler for task {task.id}: {str(e)}", exc_info=True)
             logger.error(f"❌ [TaskHandler] Error executing handler for task {task.id}: {str(e)}", exc_info=True)
-            
-            # 可选:记录错误到 Task Meta
             # await self._record_error(db, task, str(e))
             # await self._record_error(db, task, str(e))
 
 
     async def _record_error(self, db: AsyncSession, task: VasTask, error_msg: str):
     async def _record_error(self, db: AsyncSession, task: VasTask, error_msg: str):
-        """辅助方法:将错误信息回写到 Task (可选)"""
         try:
         try:
             meta = dict(task.meta) if task.meta else {}
             meta = dict(task.meta) if task.meta else {}
             meta["post_process_error"] = error_msg
             meta["post_process_error"] = error_msg
             task.meta = meta
             task.meta = meta
-            # 注意:这里不需要 commit,因为外层 manual_confirm 会统一 commit
         except Exception:
         except Exception:
             pass
             pass
 
 
 
 
-# =================================================================
-# 实例化注册表 (在其他地方 import 这个实例)
-# =================================================================
 task_processor = TaskHandlerRegistry()
 task_processor = TaskHandlerRegistry()
 
 
 
 
@@ -90,197 +74,203 @@ task_processor = TaskHandlerRegistry()
 # 具体的处理函数 (Handlers)
 # 具体的处理函数 (Handlers)
 # =================================================================
 # =================================================================
 
 
+# 辅助函数:获取用户信息(用于发通知)
+async def _get_user_by_id(db: AsyncSession, user_id: int) -> VasUser:
+    stmt = select(VasUser).where(VasUser.id == user_id)
+    result = await db.execute(stmt)
+    return result.scalar_one_or_none()
+
 @task_processor.register("auto.slot.dub.fr.tourist")
 @task_processor.register("auto.slot.dub.fr.tourist")
-async def forward_troov_appointment_letter(db: AsyncSession, task: VasTask, order: VasOrder):
+async def forward_troov_appointment_letter(db: AsyncSession, redis_client: Redis, task: VasTask, order: VasOrder):
     """
     """
-    处理邮件转发相关的任务
-    支持 routing_key: email.forward, email.auto_reply
+    Troov (法国) 预约信转发及通知
     """
     """
     # 1. 解析参数
     # 1. 解析参数
-    # 假设参数存储在 task.user_inputs 或 task.config 中
     inputs: Dict[str, Any] = task.user_inputs or {}
     inputs: Dict[str, Any] = task.user_inputs or {}
     grabbed_history = task.grabbed_history or {}
     grabbed_history = task.grabbed_history or {}
-    
     task_config = task.config or {}
     task_config = task.config or {}
 
 
-    
     email_account = "visafly666@gmail.com"
     email_account = "visafly666@gmail.com"
-    forward_to = inputs.get("email", "")
-    if not forward_to:
-        uid = order.user_id
-        stmt = select(VasUser).where(VasUser.id == uid)
-        result = await db.execute(stmt)
-        user = result.scalar_one_or_none()
-        forward_to = user.email
-        
-    recipient = task_config.get('alias_email', "")
     
     
-    first_name = inputs.get('first_name', '')
-    last_name = inputs.get('last_name', '')
+    # 获取用户以发送通知
+    user = await _get_user_by_id(db, order.user_id)
+    forward_to = inputs.get("email") or (user.email if user else "")
     
     
-    first_name = first_name.capitalize()
-    last_name = last_name.capitalize()
+    recipient = task_config.get('alias_email', "")
+    first_name = inputs.get('first_name', '').capitalize()
+    last_name = inputs.get('last_name', '').capitalize()
     
     
+    # 处理日期格式用于关键词匹配
     book_date_str = grabbed_history.get("book_date", "")
     book_date_str = grabbed_history.get("book_date", "")
-    dt = datetime.strptime(book_date_str, "%Y-%m-%dT%H:%M:%S")
-    formatted_date_time = dt.strftime("%B %-d, %Y to %Hh%M")
-    
+    formatted_date_time = ""
+    if book_date_str:
+        try:
+            # 假设 input 是 ISO 格式 "2024-03-15T09:00:00"
+            dt = datetime.strptime(book_date_str, "%Y-%m-%dT%H:%M:%S")
+            formatted_date_time = dt.strftime("%B %-d, %Y to %Hh%M") # Troov 邮件中的特殊格式
+            
+            # 为邮件通知准备通用格式
+            notification_date_str = dt.strftime("%d %b %Y, %H:%M")
+        except ValueError:
+            formatted_date_time = book_date_str
+            notification_date_str = book_date_str
+
     # 过滤条件
     # 过滤条件
-    sender = "ne-pas-repondre at consulat.gouv.fr"
-    
+    sender = "ne-pas-repondre at consulat.gouv.fr" # 注意实际匹配时可能需要处理 @ 符号
     subject_keywords = inputs.get("subjectKeywords", "Confirmed,appointment,Section,Ambassade,France,Irlande")
     subject_keywords = inputs.get("subjectKeywords", "Confirmed,appointment,Section,Ambassade,France,Irlande")
     body_keywords = inputs.get("bodyKeywords", f"Concerned,person,{first_name},{last_name},{formatted_date_time}")
     body_keywords = inputs.get("bodyKeywords", f"Concerned,person,{first_name},{last_name},{formatted_date_time}")
 
 
     if not email_account or not forward_to:
     if not email_account or not forward_to:
-        logger.warning(f"Task {task.id} missing required inputs for email forwarding.")
+        logger.warning(f"Task {task.id} missing inputs for email forwarding.")
         return
         return
 
 
     logger.info(f"Task {task.id}: Forwarding emails from {email_account} to {forward_to}")
     logger.info(f"Task {task.id}: Forwarding emails from {email_account} to {forward_to}")
 
 
-    # 2. 获取授权配置
+    # 2. 获取授权并转发邮件
     auth = await EmailAuthorizationService.get_by_email(db, email_account)
     auth = await EmailAuthorizationService.get_by_email(db, email_account)
     if not auth:
     if not auth:
-        raise ValueError(f"Email authorization not found for account: {email_account}")
+        raise ValueError(f"Email authorization not found: {email_account}")
 
 
-    # 3. 调用邮件服务执行转发
-    # 这里复用你之前写的 Service 方法
     result = await EmailAuthorizationService.forward_first_matching_email2(
     result = await EmailAuthorizationService.forward_first_matching_email2(
-        db=db,
-        auth=auth,
-        forward_to=forward_to,
-        sender=sender,
-        recipient=recipient,
-        subject_keywords=subject_keywords,
-        body_keywords=body_keywords
+        db=db, auth=auth, forward_to=forward_to, sender=sender, recipient=recipient,
+        subject_keywords=subject_keywords, body_keywords=body_keywords
     )
     )
 
 
-    # 4. (可选) 将结果回写到 Task Meta
+    # 3. 更新 Task Meta
     meta = dict(task.meta) if task.meta else {}
     meta = dict(task.meta) if task.meta else {}
     meta["forward_result"] = result
     meta["forward_result"] = result
     task.meta = meta
     task.meta = meta
     
     
-    
+    # 4. 发送预约成功通知邮件给用户 (利用 NotificationService + Redis)
+    if user and user.email:
+        logger.info(f"📧 Sending appointment confirmation email to {user.email}")
+        await NotificationService.post_email(
+            redis_client=redis_client,
+            receiver=user.email,
+            template_id="appointment_confirmation", # 对应之前定义的模板ID
+            payload={
+                "username": user.nickname or first_name,
+                "order_id": str(order.id),
+                "country": "France",
+                "city": "Dublin", # 根据 routing_key 或 inputs 推断
+                "appointment_date": formatted_date_time,
+                "visa_type": "Short Stay Any Purpose",
+                "user_email": forward_to
+            }
+        )
+
+
 @task_processor.register("auto.slot.dub.nl.tourist")
 @task_processor.register("auto.slot.dub.nl.tourist")
-async def forward_vfs_appointment_letter(db: AsyncSession, task: VasTask, order: VasOrder):
+async def forward_vfs_appointment_letter(db: AsyncSession, redis_client: Redis, task: VasTask, order: VasOrder):
     """
     """
-    处理邮件转发相关的任务
-    支持 routing_key: email.forward, email.auto_reply
+    VFS (荷兰) 预约信转发及通知
     """
     """
-    # 1. 解析参数
-    # 假设参数存储在 task.user_inputs 或 task.config 中
     inputs: Dict[str, Any] = task.user_inputs or {}
     inputs: Dict[str, Any] = task.user_inputs or {}
     grabbed_history = task.grabbed_history or {}
     grabbed_history = task.grabbed_history or {}
-    
     task_config = task.config or {}
     task_config = task.config or {}
 
 
-    
     email_account = "visafly666@gmail.com"
     email_account = "visafly666@gmail.com"
-    forward_to = inputs.get("email", "")
-    if not forward_to:
-        uid = order.user_id
-        stmt = select(VasUser).where(VasUser.id == uid)
-        result = await db.execute(stmt)
-        user = result.scalar_one_or_none()
-        forward_to = user.email
-    
+    user = await _get_user_by_id(db, order.user_id)
+    forward_to = inputs.get("email") or (user.email if user else "")
 
 
-    
     urn = grabbed_history.get("urn", "")
     urn = grabbed_history.get("urn", "")
     
     
-    # 过滤条件
     sender = "donotreply at vfsglobal.com"
     sender = "donotreply at vfsglobal.com"
     recipient = task_config.get('alias_email', "")
     recipient = task_config.get('alias_email', "")
     subject_keywords = inputs.get("subjectKeywords", "Appointment,Confirm")
     subject_keywords = inputs.get("subjectKeywords", "Appointment,Confirm")
-    body_keywords = inputs.get("bodyKeywords", f"urn")
+    body_keywords = inputs.get("bodyKeywords", f"urn") # VFS 通常用 URN 匹配
 
 
     if not email_account or not forward_to:
     if not email_account or not forward_to:
-        logger.warning(f"Task {task.id} missing required inputs for email forwarding.")
+        logger.warning(f"Task {task.id} missing inputs.")
         return
         return
 
 
-    logger.info(f"Task {task.id}: Forwarding emails from {email_account} to {forward_to}")
-
-    # 2. 获取授权配置
     auth = await EmailAuthorizationService.get_by_email(db, email_account)
     auth = await EmailAuthorizationService.get_by_email(db, email_account)
     if not auth:
     if not auth:
-        raise ValueError(f"Email authorization not found for account: {email_account}")
+        raise ValueError(f"Email auth not found: {email_account}")
 
 
-    # 3. 调用邮件服务执行转发
-    # 这里复用你之前写的 Service 方法
     result = await EmailAuthorizationService.forward_first_matching_email2(
     result = await EmailAuthorizationService.forward_first_matching_email2(
-        db=db,
-        auth=auth,
-        forward_to=forward_to,
-        sender=sender,
-        recipient=recipient,
-        subject_keywords=subject_keywords,
-        body_keywords=body_keywords
+        db=db, auth=auth, forward_to=forward_to, sender=sender, recipient=recipient,
+        subject_keywords=subject_keywords, body_keywords=body_keywords
     )
     )
 
 
-    # 4. (可选) 将结果回写到 Task Meta
     meta = dict(task.meta) if task.meta else {}
     meta = dict(task.meta) if task.meta else {}
     meta["forward_result"] = result
     meta["forward_result"] = result
     task.meta = meta
     task.meta = meta
-    
+
+    # 发送通知
+    if user and user.email:
+        await NotificationService.post_email(
+            redis_client=redis_client,
+            receiver=user.email,
+            template_id="appointment_confirmation",
+            payload={
+                "username": user.nickname or 'consumer',
+                "order_id": str(order.id),
+                "country": "Netherlands",
+                "city": "Dublin",
+                "appointment_date": "Check attachment", # VFS 可能没有直接的日期在 history
+                "visa_type": "Tourist",
+                "user_email": forward_to
+            }
+        )
+
+
 @task_processor.register("auto.slot.dub.de.tourist")
 @task_processor.register("auto.slot.dub.de.tourist")
-async def forward_visametric_appointment_letter(db: AsyncSession, task: VasTask, order: VasOrder):
+async def forward_visametric_appointment_letter(db: AsyncSession, redis_client: Redis, task: VasTask, order: VasOrder):
     """
     """
-    处理邮件转发相关的任务
-    支持 routing_key: email.forward, email.auto_reply
+    Visametric (德国) 预约信转发及通知
     """
     """
-    # 1. 解析参数
-    # 假设参数存储在 task.user_inputs 或 task.config 中
     inputs: Dict[str, Any] = task.user_inputs or {}
     inputs: Dict[str, Any] = task.user_inputs or {}
     grabbed_history = task.grabbed_history or {}
     grabbed_history = task.grabbed_history or {}
-    
     task_config = task.config or {}
     task_config = task.config or {}
 
 
-    
     email_account = "visafly666@gmail.com"
     email_account = "visafly666@gmail.com"
-    forward_to = inputs.get("email", "")
-    if not forward_to:
-        uid = order.user_id
-        stmt = select(VasUser).where(VasUser.id == uid)
-        result = await db.execute(stmt)
-        user = result.scalar_one_or_none()
-        forward_to = user.email
+    user = await _get_user_by_id(db, order.user_id)
+    forward_to = inputs.get("email") or (user.email if user else "")
     
     
     first_name = inputs.get('first_name', '')
     first_name = inputs.get('first_name', '')
     last_name = inputs.get('last_name', '')
     last_name = inputs.get('last_name', '')
-    
     pnr_number = grabbed_history.get('pnr_number', '')
     pnr_number = grabbed_history.get('pnr_number', '')
-    slot_date = grabbed_history.get('slot_date', '')
+    slot_date = grabbed_history.get('slot_date', '') # YYYY-MM-DD
     slot_time = grabbed_history.get('slot_time', '')
     slot_time = grabbed_history.get('slot_time', '')
     
     
-    # 过滤条件
     sender = "noreply at visametric.com"
     sender = "noreply at visametric.com"
     recipient = task_config.get("alias_email", "")
     recipient = task_config.get("alias_email", "")
     subject_keywords = inputs.get("subjectKeywords", f"{pnr_number},Visametric,Appointment,Request")
     subject_keywords = inputs.get("subjectKeywords", f"{pnr_number},Visametric,Appointment,Request")
     body_keywords = inputs.get("bodyKeywords", f"{pnr_number},{slot_date},{slot_time},{first_name},{last_name}")
     body_keywords = inputs.get("bodyKeywords", f"{pnr_number},{slot_date},{slot_time},{first_name},{last_name}")
 
 
     if not email_account or not forward_to:
     if not email_account or not forward_to:
-        logger.warning(f"Task {task.id} missing required inputs for email forwarding.")
+        logger.warning(f"Task {task.id} missing inputs.")
         return
         return
 
 
-    logger.info(f"Task {task.id}: Forwarding emails from {email_account} to {forward_to}")
-
-    # 2. 获取授权配置
     auth = await EmailAuthorizationService.get_by_email(db, email_account)
     auth = await EmailAuthorizationService.get_by_email(db, email_account)
     if not auth:
     if not auth:
-        raise ValueError(f"Email authorization not found for account: {email_account}")
+        raise ValueError(f"Email auth not found: {email_account}")
 
 
-    # 3. 调用邮件服务执行转发
-    # 这里复用你之前写的 Service 方法
     result = await EmailAuthorizationService.forward_first_matching_email2(
     result = await EmailAuthorizationService.forward_first_matching_email2(
-        db=db,
-        auth=auth,
-        forward_to=forward_to,
-        sender=sender,
-        recipient=recipient,
-        subject_keywords=subject_keywords,
-        body_keywords=body_keywords
+        db=db, auth=auth, forward_to=forward_to, sender=sender, recipient=recipient,
+        subject_keywords=subject_keywords, body_keywords=body_keywords
     )
     )
 
 
-    # 4. (可选) 将结果回写到 Task Meta
     meta = dict(task.meta) if task.meta else {}
     meta = dict(task.meta) if task.meta else {}
     meta["forward_result"] = result
     meta["forward_result"] = result
-    task.meta = meta
+    task.meta = meta
+
+    # 发送通知
+    if user and user.email:
+        display_date = f"{slot_date} {slot_time}" if slot_date else "Confirmed"
+        
+        await NotificationService.post_email(
+            redis_client=redis_client,
+            receiver=user.email,
+            template_id="appointment_confirmation",
+            payload={
+                "username": user.nickname or first_name,
+                "order_id": str(order.id),
+                "country": "Germany",
+                "city": "Dublin",
+                "appointment_date": display_date,
+                "visa_type": "Tourist",
+                "user_email": forward_to
+            }
+        )

+ 3 - 2
app/services/vas_task_service.py

@@ -5,6 +5,7 @@ from typing import List, Optional
 
 
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy import select, or_, and_
 from sqlalchemy import select, or_, and_
+from redis.asyncio import Redis  # 引入 Redis 类型
 
 
 
 
 from app.utils.search import apply_keyword_search_stmt
 from app.utils.search import apply_keyword_search_stmt
@@ -232,7 +233,7 @@ class VasTaskService:
         return rec
         return rec
 
 
     @staticmethod
     @staticmethod
-    async def manual_confirm(db: AsyncSession, id: int) -> VasTask:
+    async def manual_confirm(db: AsyncSession, id: int, redis_client: Redis) -> VasTask:
         stmt = select(VasTask).where(VasTask.id == id)
         stmt = select(VasTask).where(VasTask.id == id)
         result = await db.execute(stmt)
         result = await db.execute(stmt)
         task = result.scalar_one_or_none()
         task = result.scalar_one_or_none()
@@ -251,7 +252,7 @@ class VasTaskService:
 
 
         order.status = "completed"
         order.status = "completed"
         
         
-        await task_processor.execute(task.routing_key, db, task, order)
+        await task_processor.execute(task.routing_key, db, redis_client, task, order)
         await db.commit()
         await db.commit()
         await db.refresh(task)
         await db.refresh(task)
         return task
         return task

+ 137 - 0
app/tasks/notification_task.py

@@ -51,6 +51,10 @@ async def notification_consumer(session_factory, redis_client: Redis):
                     sender = "donotreply@visafly.top"
                     sender = "donotreply@visafly.top"
                     subject = "Ticket Created"
                     subject = "Ticket Created"
                     content = template_ticket_open(payload)
                     content = template_ticket_open(payload)
+                if "appointment_confirmation" == template_id:
+                    sender = "donotreply@visafly.top"
+                    subject = "Appointment Confirmation"
+                    content = template_appointment_confirmation(payload)
                 if content:
                 if content:
                     async with session_factory() as db:  # type: AsyncSession
                     async with session_factory() as db:  # type: AsyncSession
                         auth = await EmailAuthorizationService.get_by_email(db, sender)
                         auth = await EmailAuthorizationService.get_by_email(db, sender)
@@ -408,4 +412,137 @@ def template_ticket_open(payload):
                            .replace('{{ticket_url}}', str(payload.get('ticket_url', '#'))) \
                            .replace('{{ticket_url}}', str(payload.get('ticket_url', '#'))) \
                            .replace('{{app_name}}', str(payload.get('app_name', 'Visafly')))
                            .replace('{{app_name}}', str(payload.get('app_name', 'Visafly')))
 
 
+    return html_content
+
+def template_appointment_confirmation(payload):
+    """
+    生成预约成功确认邮件 (VisaFly)
+    
+    payload 需包含: 
+    - username: 用户名
+    - order_id: 订单号 (新增)
+    - country: 国家
+    - city: 城市
+    - appointment_date: 预约时间 (字符串, 例如 "2026-03-15 09:00")
+    - visa_type: 签证类型
+    - user_email: 用户邮箱 (用于提示信件已发往此处)
+    """
+    
+    # --- 1. 基础配置 (VisaFly) ---
+    company_name = "VisaFly"
+    support_email = "support@visafly.top"
+    website_home = "https://visafly.top"
+    website_contact = "https://visafly.top/refund-policy"
+
+    # --- 2. HTML 模板 ---
+    template = '''
+    <!DOCTYPE html>
+    <html>
+    <head>
+        <meta charset="UTF-8">
+        <title>Appointment Confirmed</title>
+        <style>
+            body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background-color: #f4f6f8; margin: 0; padding: 0; color: #333; }
+            .container { max-width: 600px; margin: 30px auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
+            
+            /* 头部: 品牌蓝 */
+            .header { background-color: #0056b3; padding: 30px 20px; text-align: center; color: white; }
+            .header h1 { margin: 0; font-size: 24px; font-weight: bold; }
+            .header .subtitle { margin-top: 5px; opacity: 0.9; font-size: 14px; }
+            
+            /* 正文区域 */
+            .content { padding: 30px 25px; line-height: 1.6; }
+            
+            /* 核心警告框 (黄色高亮 - 强调查收邮件) */
+            .alert-box { background-color: #fff8e1; border-left: 5px solid #ffc107; padding: 15px 20px; margin: 25px 0; border-radius: 4px; }
+            .alert-title { font-weight: bold; color: #b00020; display: block; margin-bottom: 5px; font-size: 16px; }
+            
+            /* 订单详情卡片 */
+            .details-box { background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 15px; margin-bottom: 25px; }
+            .info-row { display: flex; justify-content: space-between; margin-bottom: 10px; border-bottom: 1px dashed #e2e8f0; padding-bottom: 8px; }
+            .info-row:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
+            .label { color: #64748b; font-size: 13px; font-weight: 500; }
+            .value { font-weight: bold; color: #0f172a; font-size: 14px; text-align: right; }
+            
+            /* 底部联系方式 */
+            .help-section { background-color: #f1f5f9; padding: 20px; text-align: center; font-size: 14px; color: #475569; border-top: 1px solid #e2e8f0; }
+            .contact-link { color: #0056b3; font-weight: bold; text-decoration: none; margin: 0 5px; }
+            
+            .footer { text-align: center; padding: 20px; font-size: 12px; color: #94a3b8; }
+            a { color: #0056b3; text-decoration: none; }
+        </style>
+    </head>
+    <body>
+        <div class="container">
+            <div class="header">
+                <h1>✅ Booking Successful!</h1>
+                <div class="subtitle">Appointment Secured by {{company_name}}</div>
+            </div>
+            
+            <div class="content">
+                <p>Hello <b>{{username}}</b>,</p>
+                <p>Great news! We have successfully secured your appointment slot.</p>
+                
+                <!-- 详情卡片 -->
+                <div class="details-box">
+                    <div class="info-row">
+                        <span class="label">Order ID:</span>
+                        <span class="value">#{{order_id}}</span>
+                    </div>
+                    <div class="info-row">
+                        <span class="label">Country / City:</span>
+                        <span class="value">{{country}} - {{city}}</span>
+                    </div>
+                    <div class="info-row">
+                        <span class="label">Appointment Date:</span>
+                        <span class="value">{{appointment_date}}</span>
+                    </div>
+                    <div class="info-row">
+                        <span class="label">Visa Type:</span>
+                        <span class="value">{{visa_type}}</span>
+                    </div>
+                </div>
+
+                <!-- 核心:检查邮件提示 -->
+                <div class="alert-box">
+                    <span class="alert-title">📩 Important: Check Your Email</span>
+                    We have sent the official confirmation letter to <b>{{user_email}}</b>.<br><br>
+                    <span style="font-size: 13px;">If you don't see it in your Inbox, please check your <b>Spam/Junk</b> folder immediately.</span>
+                </div>
+            </div>
+
+            <!-- 兜底联系方式 -->
+            <div class="help-section">
+                <p style="margin-top: 0; font-weight: bold;">Did not receive the email?</p>
+                <p style="margin-bottom: 15px;">Please check your Spam folder first. If still missing, contact us:</p>
+                
+                <!-- 方式1: 邮件 -->
+                <a href="mailto:{{support_email}}" class="contact-link">✉️ Email Support</a>
+                <span style="color: #cbd5e1;">|</span>
+                <!-- 方式2: 官网 -->
+                <a href="{{website_contact}}" class="contact-link">🌐 Contact Us</a>
+            </div>
+            
+            <div class="footer">
+                &copy; 2026 {{company_name}}. All rights reserved.<br>
+                <a href="{{website_home}}">{{website_home}}</a>
+            </div>
+        </div>
+    </body>
+    </html>
+    '''
+
+    # --- 3. 执行替换 ---
+    html_content = template.replace('{{username}}', str(payload.get('username', 'Customer'))) \
+                           .replace('{{order_id}}', str(payload.get('order_id', 'N/A'))) \
+                           .replace('{{country}}', str(payload.get('country', ''))) \
+                           .replace('{{city}}', str(payload.get('city', ''))) \
+                           .replace('{{appointment_date}}', str(payload.get('appointment_date', 'Confirmed'))) \
+                           .replace('{{user_email}}', str(payload.get('user_email', 'your email'))) \
+                           .replace('{{visa_type}}', str(payload.get('visa_type', 'Standard'))) \
+                           .replace('{{company_name}}', company_name) \
+                           .replace('{{support_email}}', support_email) \
+                           .replace('{{website_contact}}', website_contact) \
+                           .replace('{{website_home}}', website_home)
+
     return html_content
     return html_content