|
|
@@ -1,548 +0,0 @@
|
|
|
-
|
|
|
-import asyncio
|
|
|
-import json
|
|
|
-from datetime import datetime
|
|
|
-from typing import Dict, Any
|
|
|
-from redis.asyncio import Redis
|
|
|
-from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
-from app.services.wechat_service import WechatService
|
|
|
-from app.services.email_authorizations_service import EmailAuthorizationService
|
|
|
-from app.utils.redis_utils import redis_qpop
|
|
|
-
|
|
|
-
|
|
|
-THROTTLE_EXPIRY = 1800
|
|
|
-
|
|
|
-async def notification_consumer(session_factory, redis_client: Redis):
|
|
|
- """
|
|
|
- 异步消费 Redis 队列 vas_notification_queue
|
|
|
- """
|
|
|
- queue_name = "vas_notification_queue"
|
|
|
- while True:
|
|
|
- try:
|
|
|
- # 阻塞获取队列消息
|
|
|
- message: Dict[str, Any] = await redis_qpop(redis_client, queue_name, timeout=5)
|
|
|
- if not message:
|
|
|
- await asyncio.sleep(1) # 队列为空,休眠
|
|
|
- continue
|
|
|
-
|
|
|
- channel = message.get("channel", "")
|
|
|
- template_id = message.get("template_id")
|
|
|
- payload = message.get("payload", {})
|
|
|
-
|
|
|
- # 按渠道发送
|
|
|
- if "email" == channel:
|
|
|
- content = None
|
|
|
- sender = None
|
|
|
- subject = None
|
|
|
- receiver = message.get("receiver", "")
|
|
|
- if "email_verification_for_bind" == template_id:
|
|
|
- sender = "donotreply@visafly.top"
|
|
|
- subject = "Email Verification"
|
|
|
- content = template_for_bind_email(payload)
|
|
|
- if "email_verification_for_reset" == template_id:
|
|
|
- sender = "donotreply@visafly.top"
|
|
|
- subject = "Reset Password"
|
|
|
- content = template_for_reset_pwd(payload)
|
|
|
- if "login_credentials" == template_id:
|
|
|
- sender = "donotreply@visafly.top"
|
|
|
- subject = "Your Account Details"
|
|
|
- content = template_for_login_credentials(payload)
|
|
|
- if "ticket_created" == template_id:
|
|
|
- sender = "donotreply@visafly.top"
|
|
|
- subject = "Ticket Created"
|
|
|
- content = template_ticket_open(payload)
|
|
|
- if "appointment_confirmation" == template_id:
|
|
|
- sender = "donotreply@visafly.top"
|
|
|
- subject = "Appointment Confirmation"
|
|
|
- content = template_appointment_confirmation(payload)
|
|
|
- if content:
|
|
|
- async with session_factory() as db: # type: AsyncSession
|
|
|
- auth = await EmailAuthorizationService.get_by_email(db, sender)
|
|
|
- send_result = await EmailAuthorizationService.send_email(auth, receiver, subject, "html", content)
|
|
|
- print(f"Email send result: {send_result}")
|
|
|
-
|
|
|
- if "wechat" == channel:
|
|
|
- api_token = "a8f79817-e18b-4739-8459-adb2ed5e2e32"
|
|
|
-
|
|
|
- if "payment_user_confirmed" == template_id:
|
|
|
- status = await WechatService.push_payment_template(api_token, payload)
|
|
|
- print(f"Wechat send status: {status}")
|
|
|
-
|
|
|
- if "slot_snapshot" == template_id:
|
|
|
- # 1. 提取标识字段
|
|
|
- country = payload.get("country", "unknown")
|
|
|
- city = payload.get("city", "unknown")
|
|
|
- visa_type = payload.get("visa_type", "unknown")
|
|
|
- earliest_date = payload.get("earliest_date", "N/A")
|
|
|
-
|
|
|
- # 2. 生成 Redis 频率限制 Key
|
|
|
- # 格式: throttle:slot_snapshot:USA:Beijing:B1
|
|
|
- throttle_key = f"throttle:slot_snapshot:{country}:{city}:{visa_type}"
|
|
|
-
|
|
|
- # 3. 检查是否存在记录(即是否在冷却期内)
|
|
|
- last_sent_val = await redis_client.get(throttle_key)
|
|
|
-
|
|
|
- # 4. 判断是否需要跳过
|
|
|
- # 如果记录存在,且 earliest_date 没有变化,则跳过推送
|
|
|
- if last_sent_val and last_sent_val == earliest_date:
|
|
|
- print(f"⏭️ Skipped redundant Wechat notification for {country}-{city} (In Cooling Period)")
|
|
|
- continue
|
|
|
-
|
|
|
- # 5. 执行发送
|
|
|
- status = await WechatService.push_slot_snapshot(api_token, payload)
|
|
|
- print(f"Wechat send status: {status}")
|
|
|
-
|
|
|
- # 6. 发送成功后更新 Redis 记录并设置过期时间
|
|
|
- # 存储当前的最早日期,下次如果日期变了,即便没过 30 分钟也会再次推送
|
|
|
- await redis_client.set(throttle_key, str(earliest_date), ex=THROTTLE_EXPIRY)
|
|
|
- print(f"✅ Notification sent: {message.get('notification_id')}")
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- print(f"⚠️ Notification consumer error: {e}")
|
|
|
- await asyncio.sleep(1) # 避免异常循环过快
|
|
|
-
|
|
|
-def template_for_bind_email(payload):
|
|
|
- """
|
|
|
- 生成绑定邮箱验证码邮件
|
|
|
-
|
|
|
- Args:
|
|
|
- payload (dict): 包含以下字段:
|
|
|
- - app_name: 应用名称 (默认 Visafly)
|
|
|
- - code: 验证码
|
|
|
- - expiration_time: 过期时间描述 (如 "10 minutes")
|
|
|
- """
|
|
|
-
|
|
|
- # 1. 定义 HTML 模板
|
|
|
- template = '''
|
|
|
- <!DOCTYPE html>
|
|
|
- <html>
|
|
|
- <head>
|
|
|
- <meta charset="UTF-8">
|
|
|
- <title>Email Verification</title>
|
|
|
- <style>
|
|
|
- body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; }
|
|
|
- .container { max-width: 600px; margin: 20px auto; background-color: #ffffff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
|
- .code { font-size: 24px; font-weight: bold; letter-spacing: 5px; color: #333; background-color: #f0f0f0; padding: 15px; text-align: center; border-radius: 4px; margin: 20px 0; }
|
|
|
- .footer { font-size: 12px; color: #888; margin-top: 30px; text-align: center; }
|
|
|
- </style>
|
|
|
- </head>
|
|
|
- <body>
|
|
|
- <div class="container">
|
|
|
- <h2>Verify your email address</h2>
|
|
|
- <p>Hello,</p>
|
|
|
- <p>You requested to bind this email address to your <strong>{{app_name}}</strong> account. Please use the verification code below to proceed:</p>
|
|
|
-
|
|
|
- <div class="code">{{code}}</div>
|
|
|
-
|
|
|
- <p>This code will expire in <strong>{{expiration_time}}</strong>.</p>
|
|
|
- <p>If you did not request this change, please ignore this email.</p>
|
|
|
-
|
|
|
- <br>
|
|
|
- <p>Best regards,<br>The {{app_name}} Team</p>
|
|
|
-
|
|
|
- <div class="footer">
|
|
|
- © 2026 {{app_name}}. All rights reserved.
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </body>
|
|
|
- </html>
|
|
|
- '''
|
|
|
-
|
|
|
- # 2. 执行数据替换
|
|
|
- # 使用 .get() 提供默认值,防止缺少字段导致报错
|
|
|
- # 使用 str() 确保数据是字符串类型
|
|
|
- app_name = str(payload.get('app_name', 'Visafly'))
|
|
|
- code = str(payload.get('code', ''))
|
|
|
- expiration_time = str(payload.get('expiration_time', '10 minutes'))
|
|
|
-
|
|
|
- # 链式替换所有占位符
|
|
|
- html_content = template.replace('{{app_name}}', app_name) \
|
|
|
- .replace('{{code}}', code) \
|
|
|
- .replace('{{expiration_time}}', expiration_time)
|
|
|
-
|
|
|
- return html_content
|
|
|
-
|
|
|
-def template_for_reset_pwd(payload):
|
|
|
- """
|
|
|
- 生成重置密码验证码邮件
|
|
|
-
|
|
|
- Args:
|
|
|
- payload (dict): 包含以下字段:
|
|
|
- - app_name: 应用名称 (默认 Visafly)
|
|
|
- - code: 验证码
|
|
|
- - expiration_time: 过期时间描述 (如 "10 minutes")
|
|
|
- """
|
|
|
-
|
|
|
- # 1. 定义 HTML 模板
|
|
|
- template = '''
|
|
|
- <!DOCTYPE html>
|
|
|
- <html>
|
|
|
- <head>
|
|
|
- <meta charset="UTF-8">
|
|
|
- <title>Reset Password</title>
|
|
|
- <style>
|
|
|
- body { font-family: 'Helvetica Neue', Arial, sans-serif; background-color: #f9f9f9; margin: 0; padding: 0; color: #333; }
|
|
|
- .container { max-width: 600px; margin: 40px auto; background-color: #ffffff; padding: 40px; border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }
|
|
|
- .header { border-bottom: 1px solid #eee; padding-bottom: 20px; margin-bottom: 30px; }
|
|
|
- .header h2 { margin: 0; color: #333; }
|
|
|
- .code { font-size: 32px; font-weight: bold; letter-spacing: 5px; color: #2563eb; background-color: #eff6ff; padding: 20px; text-align: center; border-radius: 8px; margin: 30px 0; border: 1px solid #dbeafe; }
|
|
|
- .warning { background-color: #fff7ed; border-left: 4px solid #f97316; padding: 15px; font-size: 14px; color: #c2410c; margin-top: 30px; }
|
|
|
- .footer { font-size: 12px; color: #999; margin-top: 40px; text-align: center; border-top: 1px solid #eee; padding-top: 20px; }
|
|
|
- </style>
|
|
|
- </head>
|
|
|
- <body>
|
|
|
- <div class="container">
|
|
|
- <div class="header">
|
|
|
- <h2>Password Reset Request</h2>
|
|
|
- </div>
|
|
|
-
|
|
|
- <p>Hello,</p>
|
|
|
- <p>We received a request to reset the password for your <strong>{{app_name}}</strong> account. Please use the following code to verify your identity:</p>
|
|
|
-
|
|
|
- <div class="code">{{code}}</div>
|
|
|
-
|
|
|
- <p>This code is valid for <strong>{{expiration_time}}</strong>.</p>
|
|
|
-
|
|
|
- <div class="warning">
|
|
|
- <strong>Security Tip:</strong> If you did not request a password reset, please ignore this email. No changes will be made to your account.
|
|
|
- </div>
|
|
|
-
|
|
|
- <br>
|
|
|
- <p>Best regards,<br>The {{app_name}} Team</p>
|
|
|
-
|
|
|
- <div class="footer">
|
|
|
- © 2026 {{app_name}}. All rights reserved.<br>
|
|
|
- This is an automated message, please do not reply.
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </body>
|
|
|
- </html>
|
|
|
- '''
|
|
|
-
|
|
|
- # 2. 执行数据替换
|
|
|
- # 使用 .get() 提供默认值,防止缺少字段导致报错
|
|
|
- # 使用 str() 确保数据是字符串类型,防止 replace 报错
|
|
|
- app_name = str(payload.get('app_name', 'Visafly'))
|
|
|
- code = str(payload.get('code', ''))
|
|
|
- expiration_time = str(payload.get('expiration_time', '10 minutes'))
|
|
|
-
|
|
|
- html_content = template.replace('{{app_name}}', app_name) \
|
|
|
- .replace('{{code}}', code) \
|
|
|
- .replace('{{expiration_time}}', expiration_time)
|
|
|
-
|
|
|
- return html_content
|
|
|
-
|
|
|
-def template_for_login_credentials(payload):
|
|
|
- """
|
|
|
- 生成用户登录凭证邮件 (账号 + 临时密码)
|
|
|
-
|
|
|
- Args:
|
|
|
- payload (dict): 包含以下字段:
|
|
|
- - app_name: 应用名称
|
|
|
- - username: 登录账号
|
|
|
- - password: 临时密码
|
|
|
- - login_url: 登录链接
|
|
|
- """
|
|
|
-
|
|
|
- # 1. 定义 HTML 模板
|
|
|
- template = '''
|
|
|
- <!DOCTYPE html>
|
|
|
- <html>
|
|
|
- <head>
|
|
|
- <meta charset="UTF-8">
|
|
|
- <title>Your Account Details</title>
|
|
|
- <style>
|
|
|
- body { font-family: 'Helvetica Neue', Arial, sans-serif; background-color: #f4f6f8; margin: 0; padding: 0; color: #333; }
|
|
|
- .container { max-width: 600px; margin: 40px auto; background-color: #ffffff; padding: 40px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
|
|
|
- .header { text-align: center; border-bottom: 1px solid #eee; padding-bottom: 20px; margin-bottom: 30px; }
|
|
|
- .header h1 { font-size: 24px; color: #1a1a1a; margin: 0; }
|
|
|
- .creds-box { background-color: #f0f7ff; border: 1px solid #dbeafe; border-radius: 8px; padding: 20px; margin: 25px 0; }
|
|
|
- .creds-item { margin-bottom: 10px; font-size: 16px; }
|
|
|
- .creds-label { font-weight: bold; color: #555; width: 100px; display: inline-block; }
|
|
|
- .creds-value { font-family: 'Courier New', Courier, monospace; font-weight: bold; color: #2563eb; }
|
|
|
- .btn { display: block; width: 200px; margin: 30px auto; background-color: #2563eb; color: #ffffff !important; text-align: center; padding: 12px 0; border-radius: 6px; text-decoration: none; font-weight: bold; }
|
|
|
- .note { font-size: 13px; color: #666; background-color: #fff4e5; padding: 10px; border-radius: 4px; border-left: 4px solid #f97316; }
|
|
|
- .footer { font-size: 12px; color: #999; margin-top: 40px; text-align: center; }
|
|
|
- </style>
|
|
|
- </head>
|
|
|
- <body>
|
|
|
- <div class="container">
|
|
|
- <div class="header">
|
|
|
- <h1>Welcome to {{app_name}}</h1>
|
|
|
- </div>
|
|
|
-
|
|
|
- <p>Dear User,</p>
|
|
|
- <p>Your account has been successfully set up. Below are your temporary login credentials.</p>
|
|
|
-
|
|
|
- <div class="creds-box">
|
|
|
- <div class="creds-item">
|
|
|
- <span class="creds-label">Username:</span>
|
|
|
- <span class="creds-value">{{username}}</span>
|
|
|
- </div>
|
|
|
- <div class="creds-item">
|
|
|
- <span class="creds-label">Password:</span>
|
|
|
- <span class="creds-value">{{password}}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="note">
|
|
|
- <strong>Important:</strong> For your security, please change your password immediately after logging in.
|
|
|
- </div>
|
|
|
-
|
|
|
- <a href="{{login_url}}" class="btn">Log In Now</a>
|
|
|
-
|
|
|
- <p style="text-align: center; font-size: 14px;">
|
|
|
- Or copy this link: <a href="{{login_url}}">{{login_url}}</a>
|
|
|
- </p>
|
|
|
-
|
|
|
- <div class="footer">
|
|
|
- © 2026 {{app_name}}. All rights reserved.<br>
|
|
|
- If you did not request this account, please contact support.
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </body>
|
|
|
- </html>
|
|
|
- '''
|
|
|
-
|
|
|
- # 2. 执行数据替换
|
|
|
- # 使用 payload.get() 提供默认值,防止缺少字段导致报错
|
|
|
- app_name = str(payload.get('app_name', 'Visafly'))
|
|
|
- username = str(payload.get('username', ''))
|
|
|
- password = str(payload.get('password', ''))
|
|
|
- login_url = str(payload.get('login_url', '#'))
|
|
|
-
|
|
|
- # 链式替换所有占位符
|
|
|
- html_content = template.replace('{{app_name}}', app_name) \
|
|
|
- .replace('{{username}}', username) \
|
|
|
- .replace('{{password}}', password) \
|
|
|
- .replace('{{login_url}}', login_url)
|
|
|
-
|
|
|
- return html_content
|
|
|
-
|
|
|
-def template_ticket_open(payload):
|
|
|
- """
|
|
|
- 生成工单创建通知邮件
|
|
|
- payload 需包含: username, ticket_id, ticket_type, created_at, ticket_url, app_name
|
|
|
- """
|
|
|
-
|
|
|
- # --- 1. 处理时间格式化逻辑 ---
|
|
|
- raw_time = payload.get('created_at')
|
|
|
- formatted_time = ""
|
|
|
-
|
|
|
- if isinstance(raw_time, datetime):
|
|
|
- # 如果传入的是 datetime 对象
|
|
|
- formatted_time = raw_time.strftime('%Y-%m-%d %H:%M') + " (UTC)"
|
|
|
- elif isinstance(raw_time, str):
|
|
|
- try:
|
|
|
- # 如果传入的是 ISO 字符串 (例如 '2025-12-31T02:33:00Z')
|
|
|
- # 截取前19位通常能兼容大部分 ISO 格式
|
|
|
- dt_obj = datetime.fromisoformat(raw_time.replace('Z', '+00:00'))
|
|
|
- formatted_time = dt_obj.strftime('%Y-%m-%d %H:%M') + " (UTC)"
|
|
|
- except ValueError:
|
|
|
- # 如果解析失败,直接显示原字符串
|
|
|
- formatted_time = raw_time
|
|
|
- else:
|
|
|
- formatted_time = "N/A"
|
|
|
-
|
|
|
- # --- 2. HTML 模板 ---
|
|
|
- # 注意:这里保持了 {{key}} 占位符,下面会统一替换
|
|
|
- template = '''
|
|
|
- <!DOCTYPE html>
|
|
|
- <html>
|
|
|
- <head>
|
|
|
- <meta charset="UTF-8">
|
|
|
- <title>Ticket Created</title>
|
|
|
- <style>
|
|
|
- body { font-family: 'Helvetica Neue', Arial, sans-serif; background-color: #f4f6f8; margin: 0; padding: 0; color: #333; }
|
|
|
- .container { max-width: 600px; margin: 40px auto; background-color: #ffffff; padding: 40px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
|
|
|
- .header { border-bottom: 1px solid #eee; padding-bottom: 20px; margin-bottom: 30px; }
|
|
|
- .header h1 { font-size: 22px; color: #1a1a1a; margin: 0; }
|
|
|
- .ticket-info { background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; margin: 20px 0; }
|
|
|
- .info-row { margin-bottom: 10px; display: flex; justify-content: space-between; }
|
|
|
- .info-label { color: #64748b; font-size: 14px; }
|
|
|
- .info-value { font-weight: bold; color: #0f172a; font-size: 14px; }
|
|
|
- .btn { display: block; width: 200px; margin: 30px auto; background-color: #2563eb; color: #ffffff !important; text-align: center; padding: 12px 0; border-radius: 6px; text-decoration: none; font-weight: bold; font-size: 14px; }
|
|
|
- .footer { font-size: 12px; color: #94a3b8; margin-top: 40px; text-align: center; }
|
|
|
- </style>
|
|
|
- </head>
|
|
|
- <body>
|
|
|
- <div class="container">
|
|
|
- <div class="header">
|
|
|
- <h1>Support Request Received</h1>
|
|
|
- </div>
|
|
|
-
|
|
|
- <p>Hello {{username}},</p>
|
|
|
- <p>We wanted to let you know that we've received your request. Our team is currently reviewing the details.</p>
|
|
|
-
|
|
|
- <div class="ticket-info">
|
|
|
- <div class="info-row">
|
|
|
- <span class="info-label">Ticket ID:</span>
|
|
|
- <span class="info-value">#{{ticket_id}}</span>
|
|
|
- </div>
|
|
|
- <div class="info-row">
|
|
|
- <span class="info-label">Type:</span>
|
|
|
- <span class="info-value">{{ticket_type}}</span>
|
|
|
- </div>
|
|
|
- <div class="info-row" style="margin-bottom: 0;">
|
|
|
- <span class="info-label">Created Time:</span>
|
|
|
- <!-- 使用格式化后的时间 -->
|
|
|
- <span class="info-value">{{created_at}}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <p>We usually reply within 24 hours. You will receive an email notification when our agent replies.</p>
|
|
|
-
|
|
|
- <a href="{{ticket_url}}" class="btn">View Ticket Details</a>
|
|
|
-
|
|
|
- <div class="footer">
|
|
|
- © 2025 {{app_name}}. All rights reserved.<br>
|
|
|
- Please do not reply to this email directly.
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </body>
|
|
|
- </html>
|
|
|
- '''
|
|
|
-
|
|
|
- # --- 3. 执行替换 ---
|
|
|
- # 使用 payload 中的数据替换模板占位符
|
|
|
- html_content = template.replace('{{username}}', str(payload.get('username', 'User'))) \
|
|
|
- .replace('{{ticket_id}}', str(payload.get('ticket_id', ''))) \
|
|
|
- .replace('{{ticket_type}}', str(payload.get('ticket_type', ''))) \
|
|
|
- .replace('{{created_at}}', formatted_time) \
|
|
|
- .replace('{{ticket_url}}', str(payload.get('ticket_url', '#'))) \
|
|
|
- .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">
|
|
|
- © 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
|