notification_task.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import asyncio
  2. from typing import Dict, Any
  3. from redis.asyncio import Redis
  4. from app.services.wechat_service import WechatService
  5. from app.services.email_authorizations_service import EmailAuthorizationService
  6. from app.utils.redis_utils import redis_qpop
  7. async def notification_consumer(redis_client: Redis):
  8. """
  9. 异步消费 Redis 队列 vas_notification_queue
  10. """
  11. queue_name = "vas_notification_queue"
  12. return
  13. while True:
  14. try:
  15. # 阻塞获取队列消息
  16. message: Dict[str, Any] = await redis_qpop(redis_client, queue_name, timeout=5)
  17. if not message:
  18. await asyncio.sleep(1) # 队列为空,休眠
  19. continue
  20. channels = message.get("channels", [])
  21. template_id = message.get("template_id")
  22. payload = message.get("payload", {})
  23. user_id = message.get("user_id")
  24. # 按渠道发送
  25. if "email" in channels:
  26. # EmailService.create(user_id, template_id, payload) 是你自己实现的发送逻辑
  27. await EmailAuthorizationService.send(user_id=user_id, template_id=template_id, payload=payload)
  28. if "wechat" in channels:
  29. api_token = payload.get("api_token")
  30. content = payload.get("message") or payload.get("content")
  31. if api_token and content:
  32. await WechatService.push_to_wechat({"api_token": api_token, "message": content})
  33. print(f"✅ Notification sent: {message.get('notification_id')}")
  34. except Exception as e:
  35. print(f"⚠️ Notification consumer error: {e}")
  36. await asyncio.sleep(1) # 避免异常循环过快
  37. def template_for_bind_email(payload):
  38. template = '''
  39. <!DOCTYPE html>
  40. <html>
  41. <head>
  42. <meta charset="UTF-8">
  43. <title>Email Verification</title>
  44. <style>
  45. body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; }
  46. .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); }
  47. .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; }
  48. .footer { font-size: 12px; color: #888; margin-top: 30px; text-align: center; }
  49. </style>
  50. </head>
  51. <body>
  52. <div class="container">
  53. <h2>Verify your email address</h2>
  54. <p>Hello,</p>
  55. <p>You requested to bind this email address to your <strong>{{app_name}}</strong> account. Please use the verification code below to proceed:</p>
  56. <div class="code">{{code}}</div>
  57. <p>This code will expire in <strong>{{expiration_time}}</strong>.</p>
  58. <p>If you did not request this change, please ignore this email.</p>
  59. <br>
  60. <p>Best regards,<br>The {{app_name}} Team</p>
  61. <div class="footer">
  62. &copy; 2025 {{app_name}}. All rights reserved.
  63. </div>
  64. </div>
  65. </body>
  66. </html>
  67. '''
  68. def template_for_reset_pwd(payload):
  69. template = '''
  70. <!DOCTYPE html>
  71. <html>
  72. <head>
  73. <meta charset="UTF-8">
  74. <title>Reset Password</title>
  75. <style>
  76. body { font-family: 'Helvetica Neue', Arial, sans-serif; background-color: #f9f9f9; margin: 0; padding: 0; color: #333; }
  77. .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); }
  78. .header { border-bottom: 1px solid #eee; padding-bottom: 20px; margin-bottom: 30px; }
  79. .header h2 { margin: 0; color: #333; }
  80. .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; }
  81. .warning { background-color: #fff7ed; border-left: 4px solid #f97316; padding: 15px; font-size: 14px; color: #c2410c; margin-top: 30px; }
  82. .footer { font-size: 12px; color: #999; margin-top: 40px; text-align: center; border-top: 1px solid #eee; padding-top: 20px; }
  83. </style>
  84. </head>
  85. <body>
  86. <div class="container">
  87. <div class="header">
  88. <h2>Password Reset Request</h2>
  89. </div>
  90. <p>Hello,</p>
  91. <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>
  92. <div class="code">{{code}}</div>
  93. <p>This code is valid for <strong>{{expiration_time}}</strong>.</p>
  94. <div class="warning">
  95. <strong>Security Tip:</strong> If you did not request a password reset, please ignore this email. No changes will be made to your account.
  96. </div>
  97. <br>
  98. <p>Best regards,<br>The {{app_name}} Team</p>
  99. <div class="footer">
  100. &copy; 2025 {{app_name}}. All rights reserved.<br>
  101. This is an automated message, please do not reply.
  102. </div>
  103. </div>
  104. </body>
  105. </html>
  106. '''
  107. def template_for_login_credentials(payload):
  108. template = '''
  109. <!DOCTYPE html>
  110. <html>
  111. <head>
  112. <meta charset="UTF-8">
  113. <title>Your Account Details</title>
  114. <style>
  115. body { font-family: 'Helvetica Neue', Arial, sans-serif; background-color: #f4f6f8; margin: 0; padding: 0; color: #333; }
  116. .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); }
  117. .header { text-align: center; border-bottom: 1px solid #eee; padding-bottom: 20px; margin-bottom: 30px; }
  118. .header h1 { font-size: 24px; color: #1a1a1a; margin: 0; }
  119. .creds-box { background-color: #f0f7ff; border: 1px solid #dbeafe; border-radius: 8px; padding: 20px; margin: 25px 0; }
  120. .creds-item { margin-bottom: 10px; font-size: 16px; }
  121. .creds-label { font-weight: bold; color: #555; width: 100px; display: inline-block; }
  122. .creds-value { font-family: 'Courier New', Courier, monospace; font-weight: bold; color: #2563eb; }
  123. .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; }
  124. .note { font-size: 13px; color: #666; background-color: #fff4e5; padding: 10px; border-radius: 4px; border-left: 4px solid #f97316; }
  125. .footer { font-size: 12px; color: #999; margin-top: 40px; text-align: center; }
  126. </style>
  127. </head>
  128. <body>
  129. <div class="container">
  130. <div class="header">
  131. <h1>Welcome to {{app_name}}</h1>
  132. </div>
  133. <p>Dear User,</p>
  134. <p>Your account has been successfully set up. Below are your temporary login credentials.</p>
  135. <div class="creds-box">
  136. <div class="creds-item">
  137. <span class="creds-label">Username:</span>
  138. <span class="creds-value">{{username}}</span>
  139. </div>
  140. <div class="creds-item">
  141. <span class="creds-label">Password:</span>
  142. <span class="creds-value">{{password}}</span>
  143. </div>
  144. </div>
  145. <div class="note">
  146. <strong>Important:</strong> For your security, please change your password immediately after logging in.
  147. </div>
  148. <a href="{{login_url}}" class="btn">Log In Now</a>
  149. <p style="text-align: center; font-size: 14px;">
  150. Or copy this link: <a href="{{login_url}}">{{login_url}}</a>
  151. </p>
  152. <div class="footer">
  153. &copy; 2025 {{app_name}}. All rights reserved.<br>
  154. If you did not request this account, please contact support.
  155. </div>
  156. </div>
  157. </body>
  158. </html>
  159. '''
  160. def template_ticket_open(payload):
  161. template = '''
  162. <!DOCTYPE html>
  163. <html>
  164. <head>
  165. <meta charset="UTF-8">
  166. <title>Ticket Created</title>
  167. <style>
  168. body { font-family: 'Helvetica Neue', Arial, sans-serif; background-color: #f4f6f8; margin: 0; padding: 0; color: #333; }
  169. .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); }
  170. .header { border-bottom: 1px solid #eee; padding-bottom: 20px; margin-bottom: 30px; }
  171. .header h1 { font-size: 22px; color: #1a1a1a; margin: 0; }
  172. .ticket-info { background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; margin: 20px 0; }
  173. .info-row { margin-bottom: 10px; display: flex; justify-content: space-between; }
  174. .info-label { color: #64748b; font-size: 14px; }
  175. .info-value { font-weight: bold; color: #0f172a; font-size: 14px; }
  176. .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; }
  177. .footer { font-size: 12px; color: #94a3b8; margin-top: 40px; text-align: center; }
  178. </style>
  179. </head>
  180. <body>
  181. <div class="container">
  182. <div class="header">
  183. <h1>Support Request Received</h1>
  184. </div>
  185. <p>Hello {{username}},</p>
  186. <p>We wanted to let you know that we've received your request. Our team is currently reviewing the details.</p>
  187. <div class="ticket-info">
  188. <div class="info-row">
  189. <span class="info-label">Ticket ID:</span>
  190. <span class="info-value">#{{ticket_id}}</span>
  191. </div>
  192. <div class="info-row">
  193. <span class="info-label">Type:</span>
  194. <span class="info-value">{{ticket_type}}</span>
  195. </div>
  196. <div class="info-row" style="margin-bottom: 0;">
  197. <span class="info-label">Time:</span>
  198. <span class="info-value">{{created_at}}</span>
  199. </div>
  200. </div>
  201. <p>We usually reply within 24 hours. You will receive an email notification when our agent replies.</p>
  202. <a href="{{ticket_url}}" class="btn">View Ticket Details</a>
  203. <div class="footer">
  204. &copy; 2025 {{app_name}}. All rights reserved.<br>
  205. Please do not reply to this email directly.
  206. </div>
  207. </div>
  208. </body>
  209. </html>
  210. '''
  211. def template_confirm_payment(payload):
  212. template = {
  213. "touser": "ADMIN_USER_ID",
  214. "msgtype": "textcard",
  215. "agentid": 1000001,
  216. "textcard": {
  217. "title": "💰 待确认:收到新的手动转账",
  218. "description": "<div class=\"gray\">2025-12-31 10:30:00</div> <br>订单号:ORD-20251231-001<br>用户:user@example.com<br><div class=\"highlight\">金额:¥ 3,500.00</div><br>请核实资金到账后,点击卡片确认收款。",
  219. "url": "https://admin.visafly.com/payment/confirm?payment_id=123&token=secure_token_abc",
  220. "btntxt": "立即确认"
  221. }
  222. }