|
|
@@ -1,224 +1,333 @@
|
|
|
#!/usr/bin/env python
|
|
|
# -*- coding: utf-8 -*-
|
|
|
"""
|
|
|
-阿里云国际短信发送 Demo
|
|
|
+国际短信发送服务(准实时)
|
|
|
|
|
|
-功能说明:
|
|
|
-- 使用阿里云 SMS 服务发送国际短信(2018-05-01 版本)
|
|
|
-- 服务地址:新加坡(ap-southeast-1)
|
|
|
-- 支持发送到美国、加拿大、新加坡等国家/地区
|
|
|
-- 支持 OTP(验证码)、NOTIFY(通知)、MKT(推广)三种类型
|
|
|
+说明:
|
|
|
+- 从数据库扫描待发送的验证码(area_code != +86),使用阿里云国际短信 API 发送。
|
|
|
+- 使用同步 SDK 调用并通过 asyncio.to_thread 包装为异步以不阻塞事件循环。
|
|
|
+- 配置文件:send.conf(与脚本同目录),缺失配置会尝试从环境变量读取。
|
|
|
|
|
|
-使用前准备:
|
|
|
-1. 安装依赖: pip install alibabacloud-dysmsapi20180501
|
|
|
-2. 确保已注册 Sender ID(美国、加拿大需 10 DLC 注册,新加坡需 IMDA SSIR 注册)
|
|
|
-
|
|
|
-注意: AccessKey 已直接配置在脚本中
|
|
|
-
|
|
|
-参考文档:
|
|
|
-https://next.api.aliyun.com/api/Dysmsapi/2018-05-01/SendMessageToGlobe
|
|
|
+运行: python send_international_sms.py
|
|
|
"""
|
|
|
|
|
|
+import asyncio
|
|
|
+import configparser
|
|
|
+import os
|
|
|
import sys
|
|
|
-import json
|
|
|
-from typing import Optional, Dict, Any
|
|
|
+import logging
|
|
|
+from datetime import datetime
|
|
|
+from pathlib import Path
|
|
|
+from typing import Dict, Optional, Tuple
|
|
|
+from sqlalchemy import select, update, or_, and_
|
|
|
|
|
|
try:
|
|
|
from alibabacloud_dysmsapi20180501.client import Client as Dysmsapi20180501Client
|
|
|
+ from alibabacloud_dysmsapi20180501 import models as dysms_models
|
|
|
from alibabacloud_tea_openapi import models as open_api_models
|
|
|
- from alibabacloud_dysmsapi20180501 import models as dysmsapi_20180501_models
|
|
|
from alibabacloud_tea_util import models as util_models
|
|
|
-except ImportError:
|
|
|
+except Exception:
|
|
|
print("❌ 缺少依赖包,请先安装:")
|
|
|
- print(" pip install alibabacloud-dysmsapi20180501")
|
|
|
+ print(" pip install alibabacloud-dysmsapi20180501 alibabacloud_tea_openapi alibabacloud_tea_util")
|
|
|
sys.exit(1)
|
|
|
|
|
|
+# 加载配置文件
|
|
|
+config_file = Path(__file__).parent / "send.conf"
|
|
|
+config = configparser.ConfigParser()
|
|
|
+if config_file.exists():
|
|
|
+ config.read(config_file, encoding='utf-8')
|
|
|
+else:
|
|
|
+ raise FileNotFoundError(f"配置文件不存在: {config_file}")
|
|
|
+
|
|
|
+# 导入数据库相关模块(需要从 backend 目录导入)
|
|
|
+backend_dir = Path(__file__).parent.parent / "backend"
|
|
|
+sys.path.insert(0, str(backend_dir))
|
|
|
+from database import DatabaseManager
|
|
|
+from models_db import VerificationCode
|
|
|
+
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+# ------- 从配置文件读取参数 -------
|
|
|
+# 阿里云短信配置
|
|
|
+ACCESS_KEY_ID = config.get("aliyun_sms", "access_key_id", fallback="").strip()
|
|
|
+ACCESS_KEY_SECRET = config.get("aliyun_sms", "access_key_secret", fallback="").strip()
|
|
|
+SENDER_ID = config.get("aliyun_sms", "sender_id", fallback="Lyrria")
|
|
|
+TEMPLATE_MESSAGE = config.get(
|
|
|
+ "aliyun_sms", "template_message",
|
|
|
+ fallback="[Lyrria] Your verification code is {code}. It expires in 5 minutes. Do not share this code with anyone."
|
|
|
+)
|
|
|
+ENDPOINT = config.get("aliyun_sms", "endpoint", fallback="dysmsapi.ap-southeast-1.aliyuncs.com")
|
|
|
+
|
|
|
+# 数据库配置
|
|
|
+MYSQL_HOST = config.get("database", "mysql_host", fallback="").strip()
|
|
|
+MYSQL_PORT = config.get("database", "mysql_port", fallback="").strip()
|
|
|
+MYSQL_USER = config.get("database", "mysql_user", fallback="").strip()
|
|
|
+MYSQL_PASSWORD = config.get("database", "mysql_password", fallback="").strip()
|
|
|
+MYSQL_DATABASE = config.get("database", "mysql_database", fallback="").strip()
|
|
|
+
|
|
|
+# 服务配置
|
|
|
+POLL_INTERVAL = float(config.get("service", "poll_interval", fallback="2.0"))
|
|
|
+SMS_TYPE = config.get("aliyun_sms", "sms_type", fallback="OTP") # OTP/NOTIFY/MKT
|
|
|
|
|
|
-class InternationalSMSClient:
|
|
|
- """阿里云国际短信发送客户端(2018-05-01 版本)"""
|
|
|
-
|
|
|
- # 固定配置(根据文档要求,不可修改)
|
|
|
- REGION_ID = 'ap-southeast-1' # 新加坡
|
|
|
- ENDPOINT = 'dysmsapi.ap-southeast-1.aliyuncs.com'
|
|
|
-
|
|
|
- # 短信类型
|
|
|
- TYPE_OTP = 'OTP' # 验证码
|
|
|
- TYPE_NOTIFY = 'NOTIFY' # 短信通知
|
|
|
- TYPE_MKT = 'MKT' # 推广短信
|
|
|
-
|
|
|
- def __init__(self, access_key_id: str, access_key_secret: str):
|
|
|
- """
|
|
|
- 初始化客户端
|
|
|
-
|
|
|
- Args:
|
|
|
- access_key_id: 阿里云 AccessKey ID
|
|
|
- access_key_secret: 阿里云 AccessKey Secret
|
|
|
- """
|
|
|
- # 配置客户端(直接使用 AccessKey)
|
|
|
- config = open_api_models.Config(
|
|
|
- access_key_id=access_key_id,
|
|
|
- access_key_secret=access_key_secret
|
|
|
+
|
|
|
+def create_client() -> Dysmsapi20180501Client:
|
|
|
+ """初始化阿里云国际短信客户端(从配置文件或环境读取 AccessKey)"""
|
|
|
+ if not ACCESS_KEY_ID or not ACCESS_KEY_SECRET:
|
|
|
+ raise RuntimeError(
|
|
|
+ "Missing AccessKey. Please set access_key_id and access_key_secret in send.conf or environment."
|
|
|
)
|
|
|
- config.endpoint = self.ENDPOINT
|
|
|
-
|
|
|
- self.client = Dysmsapi20180501Client(config)
|
|
|
-
|
|
|
- def send_sms(
|
|
|
- self,
|
|
|
- to: str,
|
|
|
- message: str,
|
|
|
- sender_id: str,
|
|
|
- sms_type: str = TYPE_OTP,
|
|
|
- task_id: Optional[str] = None,
|
|
|
- validity_period: Optional[int] = None,
|
|
|
- channel_id: Optional[str] = None
|
|
|
- ) -> Dict[str, Any]:
|
|
|
- """
|
|
|
- 发送国际短信
|
|
|
-
|
|
|
- Args:
|
|
|
- to: 接收短信号码,格式为国际区号+号码,例如: "8521245567****"
|
|
|
- message: 短信的完整内容(美国、加拿大需要填写报备的内容)
|
|
|
- sender_id: 发送方标识(Sender ID)
|
|
|
- - 支持数字+字母,含有字母标识最长11位,纯数字标识支持15位
|
|
|
- - 美国、加拿大:需填写 10 DLC 注册后运营商提供的 Sender ID
|
|
|
- - 新加坡:需填写在 IMDA 完成 SSIR 注册的 Sender ID
|
|
|
- sms_type: 短信类型,可选值: OTP/NOTIFY/MKT,默认 OTP
|
|
|
- task_id: 任务 ID(可选),长度不超过 255 个字符
|
|
|
- validity_period: 短信有效时长,单位:秒(可选)
|
|
|
- channel_id: 通道 ID(可选)
|
|
|
-
|
|
|
- Returns:
|
|
|
- 包含发送结果的字典,格式:
|
|
|
- {
|
|
|
- "success": bool,
|
|
|
- "response_code": str, # 短信提交状态码,如 "OK"
|
|
|
- "message_id": str, # 成功时返回消息ID
|
|
|
- "request_id": str, # 请求ID
|
|
|
- "segments": str, # 短信计费条数
|
|
|
- "response_description": str, # 状态码描述
|
|
|
- "to": str, # 接收号码
|
|
|
- "from": str, # 发送方标识
|
|
|
- "number_detail": { # 号码明细(可选)
|
|
|
- "carrier": str, # 运营商
|
|
|
- "country": str, # 国家
|
|
|
- "region": str # 地区
|
|
|
- },
|
|
|
- "error": str # 失败时返回错误信息
|
|
|
- }
|
|
|
- """
|
|
|
- # 验证短信类型
|
|
|
- if sms_type not in [self.TYPE_OTP, self.TYPE_NOTIFY, self.TYPE_MKT]:
|
|
|
- return {
|
|
|
- "success": False,
|
|
|
- "error": f"无效的短信类型: {sms_type},支持的类型: OTP/NOTIFY/MKT"
|
|
|
- }
|
|
|
-
|
|
|
- # 创建请求对象
|
|
|
- request = dysmsapi_20180501_models.SendMessageToGlobeRequest()
|
|
|
- request.to = to
|
|
|
- request.from_ = sender_id
|
|
|
- request.message = message
|
|
|
- request.type = sms_type
|
|
|
-
|
|
|
- if task_id:
|
|
|
- request.task_id = task_id
|
|
|
- if validity_period is not None:
|
|
|
- request.validity_period = validity_period
|
|
|
- if channel_id:
|
|
|
- request.channel_id = channel_id
|
|
|
-
|
|
|
- # 运行时选项
|
|
|
- runtime = util_models.RuntimeOptions()
|
|
|
-
|
|
|
- try:
|
|
|
- # 发送请求
|
|
|
- response = self.client.send_message_to_globe_with_options(request, runtime)
|
|
|
-
|
|
|
- # 解析响应
|
|
|
- body = response.body
|
|
|
-
|
|
|
- # 检查响应状态码
|
|
|
- if body.response_code == 'OK':
|
|
|
- # 成功
|
|
|
- result = {
|
|
|
- "success": True,
|
|
|
- "response_code": body.response_code,
|
|
|
- "response_description": body.response_description or '',
|
|
|
- "message_id": body.message_id or '',
|
|
|
- "request_id": body.request_id or '',
|
|
|
- "segments": body.segments or '',
|
|
|
- "to": body.to or '',
|
|
|
- "from": body.from_ or '',
|
|
|
- }
|
|
|
-
|
|
|
- # 号码明细(可选)
|
|
|
- if body.number_detail:
|
|
|
- result["number_detail"] = {
|
|
|
- "carrier": body.number_detail.carrier or '',
|
|
|
- "country": body.number_detail.country or '',
|
|
|
- "region": body.number_detail.region or ''
|
|
|
- }
|
|
|
-
|
|
|
- return result
|
|
|
- else:
|
|
|
- # 失败
|
|
|
- return {
|
|
|
- "success": False,
|
|
|
- "response_code": body.response_code or '',
|
|
|
- "response_description": body.response_description or '',
|
|
|
- "request_id": body.request_id or '',
|
|
|
- "error": body.response_description or '未知错误'
|
|
|
- }
|
|
|
-
|
|
|
- except Exception as e:
|
|
|
- return {
|
|
|
- "success": False,
|
|
|
- "error": f"发送失败: {str(e)}"
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-def main():
|
|
|
- """主函数 - 示例用法"""
|
|
|
-
|
|
|
- # AccessKey 配置(直接写死在脚本中)
|
|
|
- ACCESS_KEY_ID = 'LTAI5tQ6iFj9XMSZygRhKL4F'
|
|
|
- ACCESS_KEY_SECRET = 'eXEfRLenQfOCLWkj307jv48rBUWNHK'
|
|
|
-
|
|
|
- # 创建客户端
|
|
|
- sms_client = InternationalSMSClient(ACCESS_KEY_ID, ACCESS_KEY_SECRET)
|
|
|
-
|
|
|
- # 发送验证码短信
|
|
|
- print("📤 发送验证码短信")
|
|
|
- print("-" * 50)
|
|
|
-
|
|
|
- result = sms_client.send_sms(
|
|
|
- to="447900613287", # 接收号码(国际区号+号码)
|
|
|
- message="Lyrria,Your verification code is 123456. Valid for 5 minutes.",
|
|
|
- sender_id="Lyrria", # Sender ID
|
|
|
- sms_type=InternationalSMSClient.TYPE_OTP
|
|
|
+
|
|
|
+ client_config = open_api_models.Config(
|
|
|
+ access_key_id=ACCESS_KEY_ID,
|
|
|
+ access_key_secret=ACCESS_KEY_SECRET,
|
|
|
+ # endpoint for international API is region-specific; keep as configured
|
|
|
)
|
|
|
-
|
|
|
- if result["success"]:
|
|
|
- print(f"✅ 发送成功!")
|
|
|
- print(f" 状态码: {result.get('response_code', '')}")
|
|
|
- print(f" 状态描述: {result.get('response_description', '')}")
|
|
|
- print(f" 消息ID: {result.get('message_id', '')}")
|
|
|
- print(f" 请求ID: {result.get('request_id', '')}")
|
|
|
- print(f" 计费条数: {result.get('segments', '')}")
|
|
|
- if 'number_detail' in result:
|
|
|
- detail = result['number_detail']
|
|
|
- print(f" 运营商: {detail.get('carrier', '')}")
|
|
|
- print(f" 国家: {detail.get('country', '')}")
|
|
|
- print(f" 地区: {detail.get('region', '')}")
|
|
|
- else:
|
|
|
- print(f"❌ 发送失败: {result.get('error', '未知错误')}")
|
|
|
- if result.get('response_code'):
|
|
|
- print(f" 状态码: {result.get('response_code')}")
|
|
|
- if result.get('response_description'):
|
|
|
- print(f" 状态描述: {result.get('response_description')}")
|
|
|
-
|
|
|
-
|
|
|
-if __name__ == '__main__':
|
|
|
- main()
|
|
|
+ client_config.endpoint = ENDPOINT
|
|
|
+ return Dysmsapi20180501Client(client_config)
|
|
|
+
|
|
|
+
|
|
|
+async def send_sms_async(
|
|
|
+ client: Dysmsapi20180501Client,
|
|
|
+ to: str,
|
|
|
+ message: str,
|
|
|
+ sender_id: str,
|
|
|
+ sms_type: str = "OTP",
|
|
|
+) -> Tuple[bool, Optional[str]]:
|
|
|
+ """
|
|
|
+ 发送国际短信(异步包装同步 SDK)
|
|
|
+ Returns: (success, error_message)
|
|
|
+ """
|
|
|
+ request = dysms_models.SendMessageToGlobeRequest()
|
|
|
+ # 阿里云国际接口要求不带前导 '+',去掉可能的 '+' 和空白
|
|
|
+ normalized_to = (to or "").lstrip("+").strip()
|
|
|
+ request.to = normalized_to
|
|
|
+ request.from_ = sender_id
|
|
|
+ request.message = message
|
|
|
+ request.type = sms_type
|
|
|
+
|
|
|
+ try:
|
|
|
+ resp = await asyncio.to_thread(
|
|
|
+ client.send_message_to_globe_with_options,
|
|
|
+ request,
|
|
|
+ util_models.RuntimeOptions()
|
|
|
+ )
|
|
|
+
|
|
|
+ body = getattr(resp, "body", None)
|
|
|
+ if body and getattr(body, "response_code", None) == "OK":
|
|
|
+ logger.info(f"✅ 国际短信发送成功: to={to}, message_id={getattr(body, 'message_id', '')}")
|
|
|
+ return True, None
|
|
|
+ else:
|
|
|
+ error_desc = getattr(body, "response_description", "") if body else "No response body"
|
|
|
+ error_code = getattr(body, "response_code", "") if body else ""
|
|
|
+ error_msg = f"Code: {error_code}, Desc: {error_desc}"
|
|
|
+ logger.error(f"❌ 国际短信发送失败: to={to}, {error_msg}")
|
|
|
+ return False, error_msg
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_msg = f"Unexpected error: {e}"
|
|
|
+ logger.error(f"❌ 国际短信发送异常: to={to}, {error_msg}", exc_info=True)
|
|
|
+ return False, error_msg
|
|
|
+
|
|
|
+
|
|
|
+async def process_international_sms_codes(client: Dysmsapi20180501Client, db_manager: DatabaseManager):
|
|
|
+ """扫描并发送国际短信验证码(area_code != +86)"""
|
|
|
+ async with db_manager.get_session() as session:
|
|
|
+ result = await session.execute(
|
|
|
+ select(VerificationCode)
|
|
|
+ .where(
|
|
|
+ or_(
|
|
|
+ VerificationCode.send_type == "international_sms",
|
|
|
+ and_(
|
|
|
+ VerificationCode.send_type == "domestic_sms",
|
|
|
+ VerificationCode.area_code != "+86",
|
|
|
+ )
|
|
|
+ ),
|
|
|
+ VerificationCode.is_sent == False,
|
|
|
+ VerificationCode.is_valid == True,
|
|
|
+ VerificationCode.phone.isnot(None)
|
|
|
+ )
|
|
|
+ .order_by(VerificationCode.created_at.asc())
|
|
|
+ .limit(50)
|
|
|
+ )
|
|
|
+ codes = result.scalars().all()
|
|
|
+
|
|
|
+ if not codes:
|
|
|
+ return 0, 0
|
|
|
+
|
|
|
+ logger.info(f"📋 找到 {len(codes)} 条待发送的国际短信验证码")
|
|
|
+ success_count = 0
|
|
|
+ fail_count = 0
|
|
|
+
|
|
|
+ for code_record in codes:
|
|
|
+ try:
|
|
|
+ area_code = (code_record.area_code or "").strip()
|
|
|
+ phone = (code_record.phone or "").strip()
|
|
|
+ code_value = code_record.code
|
|
|
+ record_id = code_record.id
|
|
|
+
|
|
|
+ if not phone:
|
|
|
+ raise ValueError("empty phone")
|
|
|
+ if area_code.startswith("+"):
|
|
|
+ to = f"{area_code}{phone}"
|
|
|
+ elif area_code:
|
|
|
+ to = f"+{area_code}{phone}"
|
|
|
+ else:
|
|
|
+ to = phone
|
|
|
+
|
|
|
+ message = TEMPLATE_MESSAGE.format(code=code_value)
|
|
|
+
|
|
|
+ success, error_msg = await send_sms_async(
|
|
|
+ client,
|
|
|
+ to,
|
|
|
+ message,
|
|
|
+ SENDER_ID,
|
|
|
+ sms_type=SMS_TYPE
|
|
|
+ )
|
|
|
+
|
|
|
+ now = datetime.now()
|
|
|
+ if success:
|
|
|
+ await session.execute(
|
|
|
+ update(VerificationCode)
|
|
|
+ .where(VerificationCode.id == record_id)
|
|
|
+ .values(
|
|
|
+ is_sent=True,
|
|
|
+ sent_at=now,
|
|
|
+ metadata_json={
|
|
|
+ **(code_record.metadata_json or {}),
|
|
|
+ "send_status": "success",
|
|
|
+ "send_at": now.isoformat(),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ )
|
|
|
+ success_count += 1
|
|
|
+ else:
|
|
|
+ await session.execute(
|
|
|
+ update(VerificationCode)
|
|
|
+ .where(VerificationCode.id == record_id)
|
|
|
+ .values(
|
|
|
+ is_valid=False,
|
|
|
+ metadata_json={
|
|
|
+ **(code_record.metadata_json or {}),
|
|
|
+ "send_status": "failed",
|
|
|
+ "error": error_msg,
|
|
|
+ "send_at": now.isoformat(),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ )
|
|
|
+ fail_count += 1
|
|
|
+
|
|
|
+ await session.commit()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ 处理国际验证码失败: id={getattr(code_record, 'id', 'n/a')}, error={e}", exc_info=True)
|
|
|
+ try:
|
|
|
+ now = datetime.now()
|
|
|
+ await session.execute(
|
|
|
+ update(VerificationCode)
|
|
|
+ .where(VerificationCode.id == getattr(code_record, "id", None))
|
|
|
+ .values(
|
|
|
+ is_valid=False,
|
|
|
+ metadata_json={
|
|
|
+ **(getattr(code_record, "metadata_json", {}) or {}),
|
|
|
+ "send_status": "error",
|
|
|
+ "error": str(e),
|
|
|
+ "send_at": now.isoformat(),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ )
|
|
|
+ await session.commit()
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+ fail_count += 1
|
|
|
+
|
|
|
+ if success_count > 0 or fail_count > 0:
|
|
|
+ logger.info(f"📊 处理完成: 成功 {success_count} 条, 失败 {fail_count} 条")
|
|
|
+
|
|
|
+ return success_count, fail_count
|
|
|
+
|
|
|
+
|
|
|
+async def main_loop():
|
|
|
+ """主循环:准实时轮询并发送国际验证码"""
|
|
|
+ # 设置数据库配置到环境变量(DatabaseManager 从环境变量读取)
|
|
|
+ if MYSQL_HOST:
|
|
|
+ os.environ["MYSQL_HOST"] = MYSQL_HOST
|
|
|
+ if MYSQL_PORT:
|
|
|
+ os.environ["MYSQL_PORT"] = MYSQL_PORT
|
|
|
+ if MYSQL_USER:
|
|
|
+ os.environ["MYSQL_USER"] = MYSQL_USER
|
|
|
+ if MYSQL_PASSWORD:
|
|
|
+ os.environ["MYSQL_PASSWORD"] = MYSQL_PASSWORD
|
|
|
+ if MYSQL_DATABASE:
|
|
|
+ os.environ["MYSQL_DATABASE"] = MYSQL_DATABASE
|
|
|
+
|
|
|
+ db_manager = DatabaseManager()
|
|
|
+ client = None
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 初始化数据库连接
|
|
|
+ await db_manager.connect()
|
|
|
+ logger.info("✅ 数据库连接成功")
|
|
|
+
|
|
|
+ # 初始化短信客户端
|
|
|
+ client = create_client()
|
|
|
+ logger.info("✅ 阿里云国际短信客户端初始化成功")
|
|
|
+
|
|
|
+ logger.info(f"🚀 开始准实时发送国际短信服务(轮询间隔: {POLL_INTERVAL}秒)...")
|
|
|
+
|
|
|
+ # 主循环:持续轮询
|
|
|
+ while True:
|
|
|
+ try:
|
|
|
+ await process_international_sms_codes(client, db_manager)
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ 处理国际验证码时出错: {e}", exc_info=True)
|
|
|
+
|
|
|
+ # 等待下次轮询
|
|
|
+ await asyncio.sleep(POLL_INTERVAL)
|
|
|
+
|
|
|
+ except KeyboardInterrupt:
|
|
|
+ logger.info("🛑 收到中断信号,正在关闭...")
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ 服务异常: {e}", exc_info=True)
|
|
|
+ sys.exit(1)
|
|
|
+ finally:
|
|
|
+ await db_manager.disconnect()
|
|
|
+ logger.info("✅ 服务已关闭")
|
|
|
+
|
|
|
+
|
|
|
+def setup_logging():
|
|
|
+ """配置日志:正常日志输出到 send_international_sms.log,错误日志输出到 send_international_sms_error.log"""
|
|
|
+ log_dir = Path(__file__).parent
|
|
|
+ log_file = log_dir / "send_international_sms.log"
|
|
|
+ error_log_file = log_dir / "send_international_sms_error.log"
|
|
|
+
|
|
|
+ formatter = logging.Formatter(
|
|
|
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
|
+ datefmt='%Y-%m-%d %H:%M:%S'
|
|
|
+ )
|
|
|
+
|
|
|
+ root_logger = logging.getLogger()
|
|
|
+ root_logger.setLevel(logging.INFO)
|
|
|
+ root_logger.handlers.clear()
|
|
|
+
|
|
|
+ console_handler = logging.StreamHandler(sys.stdout)
|
|
|
+ console_handler.setLevel(logging.INFO)
|
|
|
+ console_handler.setFormatter(formatter)
|
|
|
+ root_logger.addHandler(console_handler)
|
|
|
+
|
|
|
+ file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
|
|
+ file_handler.setLevel(logging.INFO)
|
|
|
+ file_handler.setFormatter(formatter)
|
|
|
+ root_logger.addHandler(file_handler)
|
|
|
+
|
|
|
+ error_file_handler = logging.FileHandler(error_log_file, encoding='utf-8')
|
|
|
+ error_file_handler.setLevel(logging.ERROR)
|
|
|
+ error_file_handler.setFormatter(formatter)
|
|
|
+ root_logger.addHandler(error_file_handler)
|
|
|
+
|
|
|
+ logger.info(f"✅ 日志配置完成:正常日志 -> {log_file}, 错误日志 -> {error_log_file}")
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ setup_logging()
|
|
|
+ asyncio.run(main_loop())
|
|
|
|