Kaynağa Gözat

提交代码

zlwu 10 ay önce
ebeveyn
işleme
b5a1eb099d

+ 16 - 5
backend/database.py

@@ -195,21 +195,28 @@ class DatabaseManager:
         """后台重建Redis连接(不阻塞主流程)"""
         start_time = time.time()
         logger.info("🔧 开始后台重建Redis连接...")
+        old_client = None
         try:
             old_client = self.redis_client
             self.redis_client = None  # 先清空,避免其他请求使用旧连接
             await self._create_redis_client()
-            if old_client:
-                try:
-                    await old_client.close()
-                except:
-                    pass
             elapsed = (time.time() - start_time) * 1000
             logger.info(f"✅ Redis连接已在后台重建完成,耗时: {elapsed:.1f}ms")
+        except asyncio.CancelledError:
+            # 🔥 任务被取消时,确保清理资源
+            logger.info("Redis重建任务被取消")
+            raise  # 重新抛出 CancelledError
         except Exception as e:
             elapsed = (time.time() - start_time) * 1000
             logger.error(f"❌ 后台重建Redis连接失败,耗时: {elapsed:.1f}ms, 错误: {e}")
             logger.error(traceback.format_exc())
+        finally:
+            # 🔥 确保旧连接被关闭
+            if old_client:
+                try:
+                    await old_client.close()
+                except Exception:
+                    pass
     
     def _start_healthcheck_task(self):
         """启动后台健康检查任务"""
@@ -229,6 +236,10 @@ class DatabaseManager:
                     # 检查Redis(MySQL由SQLAlchemy的pool_pre_ping自动处理)
                     await self._ensure_redis_client()
                     
+                except asyncio.CancelledError:
+                    # 🔥 任务被取消时,确保清理资源
+                    logger.info("数据库健康检查任务被取消")
+                    raise  # 重新抛出 CancelledError
                 except Exception as e:
                     logger.error(f"❌ 健康检查任务异常: {e}")
         

+ 13 - 0
backend/websocket_handler.py

@@ -1173,6 +1173,10 @@ class WebSocketHandler:
             if current_round > 0:
                 await self.redis_cache.set_current_round(self.user_id, current_round)
             
+        except asyncio.CancelledError:
+            # 🔥 任务被取消时,确保清理资源
+            logger.debug(f"Redis上下文预加载任务被取消: request_id={request_id}")
+            raise  # 重新抛出 CancelledError
         except Exception as e:
             import traceback
             traceback.print_exc()
@@ -1272,6 +1276,10 @@ class WebSocketHandler:
                         f"user={item['user_id']}, round={item['round_number']}"
                     )
                 
+            except asyncio.CancelledError:
+                # 🔥 任务被取消时,确保清理资源
+                logger.info("批量持久化任务被取消,正在清理...")
+                raise  # 重新抛出 CancelledError
             except Exception as e:
                 import traceback
                 traceback.print_exc()
@@ -1346,6 +1354,7 @@ class WebSocketHandler:
                     from models_db import ConversationSummary
                     from sqlalchemy import select
                     
+                    # 🔥 async with 会自动管理 session 生命周期,包括 CancelledError 情况
                     async with db_manager.get_session() as session:
                         result = await session.execute(
                             select(ConversationSummary)
@@ -1374,6 +1383,10 @@ class WebSocketHandler:
                     import traceback
                     traceback.print_exc()
                     
+            except asyncio.CancelledError:
+                # 🔥 任务被取消时,确保清理资源
+                logger.info("总结生成任务被取消,正在清理...")
+                raise  # 重新抛出 CancelledError
             except Exception as e:
                 import traceback
                 traceback.print_exc()

+ 130 - 0
sendmsg/docs/阿里云短信服务Java SDK调用指南.md

@@ -0,0 +1,130 @@
+# 阿里云短信服务 Java SDK 调用指南
+
+## 前置准备
+
+| 步骤 | 说明 | 参考链接 |
+|------|------|----------|
+| 1. 查看 API 文档 | 了解 SendSms 接口参数 | [SendSms API](https://help.aliyun.com/zh/sms/developer-reference/api-dysmsapi-2017-05-25-sendsms) |
+| 2. 创建 RAM 用户 | 建议使用 RAM 用户而非主账号 | [RAM 用户概览](https://help.aliyun.com/zh/ram/user-guide/overview-of-ram-users) |
+| 3. 创建 AccessKey | 获取 AccessKey ID 和 Secret | [创建访问密钥](https://help.aliyun.com/zh/ram/user-guide/create-an-accesskey-pair) |
+
+## 环境配置
+
+### Java 版本要求
+- Java 8 或更高版本
+
+### Maven 依赖
+
+```xml
+<dependency>
+    <groupId>com.aliyun</groupId>
+    <artifactId>dysmsapi20170525</artifactId>
+    <version>the-latest-version</version>
+</dependency>
+```
+
+> 最新版本号:https://mvnrepository.com/artifact/com.aliyun/dysmsapi20170525
+
+### 环境变量配置
+
+| 环境变量名 | 说明 | 示例值 |
+|-----------|------|--------|
+| `ALIBABA_CLOUD_ACCESS_KEY_ID` | AccessKey ID | `LTAI5t...` |
+| `ALIBABA_CLOUD_ACCESS_KEY_SECRET` | AccessKey Secret | `xxx...` |
+
+> 配置方法:[Linux/macOS/Windows 环境变量配置](https://help.aliyun.com/zh/sdk/developer-reference/configure-the-alibaba-cloud-accesskey-environment-variable-on-linux-macos-and-windows-systems)
+
+## 代码示例
+
+```java
+package com.aliyun.sample;
+
+import com.aliyun.teaopenapi.models.Config;
+import com.aliyun.dysmsapi20170525.Client;
+import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
+import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
+import static com.aliyun.teautil.Common.toJSONString;
+
+public class Sample {
+    public static Client createClient() throws Exception {
+        Config config = new Config()
+            // 从环境变量读取 AccessKey ID
+            .setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))
+            // 从环境变量读取 AccessKey Secret
+            .setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"));
+        
+        // 配置 Endpoint
+        config.endpoint = "dysmsapi.aliyuncs.com";
+        
+        return new Client(config);
+    }
+
+    public static void main(String[] args) throws Exception {
+        // 初始化客户端
+        Client client = Sample.createClient();
+        
+        // 构造请求对象
+        SendSmsRequest sendSmsRequest = new SendSmsRequest()
+            .setPhoneNumbers("1390000****")           // 接收短信的手机号
+            .setSignName("阿里云")                      // 短信签名
+            .setTemplateCode("SMS_15305****")          // 短信模板代码
+            .setTemplateParam("{\"name\":\"张三\",\"number\":\"1390000****\"}"); // 模板参数(JSON 字符串)
+        
+        // 发送短信
+        SendSmsResponse sendSmsResponse = client.sendSms(sendSmsRequest);
+        
+        // 输出响应
+        System.out.println(toJSONString(sendSmsResponse));
+    }
+}
+```
+
+## 请求参数说明
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `PhoneNumbers` | String | 是 | 接收短信的手机号,多个号码用逗号分隔 |
+| `SignName` | String | 是 | 短信签名,需在控制台审核通过 |
+| `TemplateCode` | String | 是 | 短信模板代码,需在控制台审核通过 |
+| `TemplateParam` | String | 否 | 模板参数,JSON 格式字符串 |
+
+## 响应格式
+
+```json
+{
+  "headers": {
+    "x-acs-request-id": "F59B3F78-D9CD-5D01-A9CC-AE5C921ED9C0",
+    "content-type": "application/json;charset=utf-8"
+  },
+  "statusCode": 200,
+  "body": {
+    "bizId": "695425589610909881^0",
+    "code": "OK",
+    "message": "OK",
+    "requestId": "F59B3F78-D9CD-5D01-A9CC-AE5C921ED9C0"
+  }
+}
+```
+
+### 响应字段说明
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `bizId` | String | 业务流水号,用于查询发送状态 |
+| `code` | String | 状态码,`OK` 表示成功 |
+| `message` | String | 状态描述 |
+| `requestId` | String | 请求 ID,用于问题排查 |
+
+## 注意事项
+
+1. **安全建议**:使用环境变量存储 AccessKey,避免硬编码
+2. **RAM 用户**:建议使用 RAM 用户而非主账号进行 API 调用
+3. **签名和模板**:需在阿里云控制台提前申请并审核通过
+4. **模板参数**:必须是有效的 JSON 字符串,参数名需与模板中的变量名一致
+
+## 相关链接
+
+- [API 概览](https://help.aliyun.com/zh/sms/developer-reference/api-dysmsapi-2017-05-25-overview)
+- [SDK 下载](https://api.aliyun.com/api-tools/sdk/Dysmsapi?version=2017-05-25&language=java-tea)
+- [常见问题](https://help.aliyun.com/zh/sms/developer-reference/api-sdk-faq)
+

+ 207 - 0
sendmsg/docs/阿里云短信服务Python SDK调用指南.md

@@ -0,0 +1,207 @@
+# 阿里云短信服务 Python SDK 调用指南
+
+## 前置准备
+
+| 步骤 | 说明 | 参考链接 |
+|------|------|----------|
+| 1. 查看 API 文档 | 了解 SendSms 接口参数 | [API 文档](https://next.api.aliyun.com/api/Dysmsapi/2017-05-25/?tab=DOC) |
+| 2. 创建 RAM 用户 | 建议使用 RAM 用户而非主账号 | [RAM 用户概览](https://help.aliyun.com/zh/ram/user-guide/overview-of-ram-users) |
+| 3. 创建 AccessKey | 获取 AccessKey ID 和 Secret | [创建访问密钥](https://help.aliyun.com/zh/ram/user-guide/create-an-accesskey-pair) |
+
+## 环境配置
+
+### Python 版本要求
+- Python >= 3.7
+
+### 安装 SDK
+
+```bash
+pip install alibabacloud_dysmsapi20170525==4.3.0
+```
+
+> 最新版本:https://pypi.org/project/alibabacloud_dysmsapi20170525/
+
+### 环境变量配置
+
+| 环境变量名 | 说明 | 示例值 |
+|-----------|------|--------|
+| `ALIBABA_CLOUD_ACCESS_KEY_ID` | AccessKey ID | `LTAI5t...` |
+| `ALIBABA_CLOUD_ACCESS_KEY_SECRET` | AccessKey Secret | `xxx...` |
+
+> 推荐使用无 AK 方式(凭据配置),详见:[凭据配置文档](https://help.aliyun.com/document_detail/378659.html)
+
+## 代码示例
+
+### 同步调用示例
+
+```python
+# -*- coding: utf-8 -*-
+import os
+import sys
+from typing import List
+from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
+from alibabacloud_credentials.client import Client as CredentialClient
+from alibabacloud_tea_openapi import models as open_api_models
+from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
+from alibabacloud_tea_util import models as util_models
+from alibabacloud_tea_util.client import Client as UtilClient
+
+class Sample:
+    @staticmethod
+    def create_client() -> Dysmsapi20170525Client:
+        """
+        使用凭据初始化账号 Client
+        @return: Client
+        """
+        # 使用凭据客户端(推荐方式,支持无 AK)
+        credential = CredentialClient()
+        config = open_api_models.Config(
+            credential=credential
+        )
+        # 配置 Endpoint
+        config.endpoint = 'dysmsapi.aliyuncs.com'
+        return Dysmsapi20170525Client(config)
+    
+    @staticmethod
+    def main(args: List[str]) -> None:
+        # 初始化客户端
+        client = Sample.create_client()
+        
+        # 构造请求对象
+        send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
+            phone_numbers='1390000****',           # 接收短信的手机号
+            sign_name='阿里云',                    # 短信签名
+            template_code='SMS_15305****',         # 短信模板代码
+            template_param='{"name":"张三","number":"1390000****"}'  # 模板参数(JSON 字符串)
+        )
+        
+        try:
+            # 发送短信
+            response = client.send_sms_with_options(
+                send_sms_request, 
+                util_models.RuntimeOptions()
+            )
+            # 打印响应
+            print(response)
+        except Exception as error:
+            # 错误处理
+            print(f"错误信息: {error.message}")
+            if hasattr(error, 'data') and error.data:
+                print(f"诊断地址: {error.data.get('Recommend')}")
+            UtilClient.assert_as_string(error.message)
+
+if __name__ == '__main__':
+    Sample.main(sys.argv[1:])
+```
+
+### 异步调用示例
+
+```python
+import asyncio
+from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
+from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
+from alibabacloud_tea_util import models as util_models
+
+class Sample:
+    @staticmethod
+    async def main_async(args: List[str]) -> None:
+        client = Sample.create_client()
+        send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
+            phone_numbers='1390000****',
+            sign_name='阿里云',
+            template_code='SMS_15305****',
+            template_param='{"name":"张三"}'
+        )
+        
+        try:
+            # 异步调用
+            response = await client.send_sms_with_options_async(
+                send_sms_request, 
+                util_models.RuntimeOptions()
+            )
+            print(response)
+        except Exception as error:
+            print(f"错误信息: {error.message}")
+
+# 运行异步示例
+if __name__ == '__main__':
+    asyncio.run(Sample.main_async(sys.argv[1:]))
+```
+
+### 使用环境变量直接配置(传统方式)
+
+```python
+import os
+from alibabacloud_tea_openapi import models as open_api_models
+
+# 直接使用 AccessKey(不推荐,建议使用凭据客户端)
+config = open_api_models.Config(
+    access_key_id=os.environ['ALIBABA_CLOUD_ACCESS_KEY_ID'],
+    access_key_secret=os.environ['ALIBABA_CLOUD_ACCESS_KEY_SECRET']
+)
+config.endpoint = 'dysmsapi.aliyuncs.com'
+```
+
+## 请求参数说明
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `phone_numbers` | String | 是 | 接收短信的手机号,多个号码用逗号分隔 |
+| `sign_name` | String | 是 | 短信签名,需在控制台审核通过 |
+| `template_code` | String | 是 | 短信模板代码,需在控制台审核通过 |
+| `template_param` | String | 否 | 模板参数,JSON 格式字符串 |
+
+## 响应格式
+
+```python
+# 响应对象结构
+response.body.request_id      # 请求 ID
+response.body.biz_id          # 业务流水号
+response.body.code            # 状态码(OK 表示成功)
+response.body.message         # 状态描述
+```
+
+### 响应字段说明
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `biz_id` | String | 业务流水号,用于查询发送状态 |
+| `code` | String | 状态码,`OK` 表示成功 |
+| `message` | String | 状态描述 |
+| `request_id` | String | 请求 ID,用于问题排查 |
+
+## 异常处理
+
+```python
+from Tea.exceptions import UnretryableException, TeaException
+
+try:
+    response = client.send_sms_with_options(request, util_models.RuntimeOptions())
+except UnretryableException as e:
+    # 网络异常
+    print(f"网络异常: {e}")
+except TeaException as e:
+    # 业务异常
+    print(f"业务异常: {e.message}")
+    if e.data:
+        print(f"诊断地址: {e.data.get('Recommend')}")
+except Exception as e:
+    # 其他异常
+    print(f"其他异常: {e}")
+```
+
+## 注意事项
+
+1. **凭据配置**:推荐使用 `CredentialClient` 进行无 AK 配置,更安全
+2. **环境变量**:如使用传统方式,确保设置 `ALIBABA_CLOUD_ACCESS_KEY_ID` 和 `ALIBABA_CLOUD_ACCESS_KEY_SECRET`
+3. **签名和模板**:需在阿里云控制台提前申请并审核通过
+4. **模板参数**:必须是有效的 JSON 字符串,参数名需与模板中的变量名一致
+5. **异步调用**:使用 `send_sms_with_options_async` 进行异步调用,需要配合 `asyncio`
+
+## 相关链接
+
+- [SDK 源码仓库](https://github.com/aliyun/alibabacloud-python-sdk/tree/master/dysmsapi-20170525)
+- [SDK 示例代码](https://next.api.aliyun.com/api/Dysmsapi/2017-05-25/?tab=DEMO&lang=PYTHON&sdkStyle=dara)
+- [API 文档](https://next.api.aliyun.com/api/Dysmsapi/2017-05-25/?tab=DOC)
+- [凭据配置文档](https://help.aliyun.com/document_detail/378659.html)
+

+ 56 - 0
sendmsg/docs/阿里云邮件SMTP发送简要指南.md

@@ -0,0 +1,56 @@
+# 阿里云邮件 SMTP 发送(Python 3.6+)
+
+面向通过 SMTP 使用阿里云邮件推送发送邮件的最小步骤,来源于官方示例文档。
+
+## 1. 前置准备
+- 开通邮件推送并创建发信地址、SMTP 密码。
+- 申请并配置回信地址(可选)、退信地址(Return-Path)。
+- Python 3.6+,内置 `smtplib`/`email` 模块即可。
+
+## 2. SMTP 配置
+| 项 | 值 |
+| --- | --- |
+| 主机 | `smtpdm.aliyun.com` |
+| 端口 | 25 / 80(明文),465(SSL) |
+| 认证 | 控制台发信地址 + SMTP 密码 |
+| 收件人数上限 | 单次不超过 60 人(含 To/Cc/Bcc) |
+
+## 3. 最小代码示例
+```python
+import smtplib, email
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from email.header import Header
+from email.utils import formataddr
+
+USERNAME = "your_sender@example.com"   # 发信地址
+PASSWORD = "your_smtp_password"        # SMTP 密码
+RCPT_TO = ["to1@example.com"]          # To 收件人
+
+msg = MIMEMultipart("alternative")
+msg["Subject"] = Header("自定义主题")
+msg["From"] = formataddr(["自定义昵称", USERNAME])
+msg["To"] = ",".join(RCPT_TO)
+msg.attach(MIMEText("HTML 或纯文本内容", "html", "utf-8"))
+
+client = smtplib.SMTP_SSL("smtpdm.aliyun.com", 465)  # 明文用 SMTP + 25/80
+client.login(USERNAME, PASSWORD)
+client.sendmail(USERNAME, RCPT_TO, msg.as_string())
+client.quit()
+```
+
+## 4. 常用可选头
+- `Reply-To`:收信人回复的地址。
+- `Return-Path`:退信回执地址。
+- `Message-Id`/`Date`:使用 `email.utils` 生成以满足 RFC 5322。
+- `X-AliDM-Trace`:开启打开/链接跟踪,需在控制台预先创建 Tag。
+
+## 5. 异常排查
+- 连接失败:检查端口可达性,必要时改用 SSL 465。
+- 认证失败:确认发信地址与 SMTP 密码匹配。
+- 收件人被拒绝:校验地址合法性与配额,单次≤60 人。
+- 数据被拒绝:检查邮件内容、附件大小与合规性。
+
+## 6. 参考
+- 官方示例与端口说明(Python 3.6+):https://help.aliyun.com/zh/direct-mail/smtp-python3-6
+

+ 40 - 0
sendmsg/send.conf

@@ -0,0 +1,40 @@
+# 验证码发送服务配置文件
+# 用于 send_sms_gn.py 和 send_email.py
+#
+# 说明:
+# 1. 如果配置项为空,脚本会尝试从环境变量读取(兼容 backend/.env)
+# 2. 数据库配置:如果此处未配置,会从环境变量 MYSQL_* 读取
+# 3. 敏感信息(如密码)建议使用环境变量,避免泄露
+
+[aliyun_sms]
+# 阿里云短信服务配置
+# access_key_id 和 access_key_secret 如果为空,会从环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET 读取
+access_key_id = LTAI5tQ6iFj9XMSZygRhKL4F
+access_key_secret = eXEfRLenQfOCLWkj307jv48rBUWNHK 
+sign_name = 灵语科技
+template_code = SMS_498720756
+endpoint = dysmsapi.aliyuncs.com
+
+[smtp]
+# 阿里云 DirectMail SMTP 配置
+# smtp_username 和 smtp_password 如果为空,会从环境变量 SMTP_USERNAME 和 SMTP_PASSWORD 读取
+smtp_host = smtpdm.aliyun.com
+smtp_port_ssl = 465
+smtp_username = support@mail.lyrria.com
+smtp_password = LRrria123456 
+email_subject = Lyrria 注册登录验证码
+
+[database]
+# MySQL 数据库配置
+# 如果此处未配置,会从环境变量 MYSQL_* 读取(兼容 backend/.env)
+mysql_host = 101.201.60.170
+mysql_port = 13306
+mysql_user = root
+mysql_password = Lyrria@Root@123456
+mysql_database = Lyrria 
+
+[service]
+# 服务配置
+# 轮询间隔(秒),准实时发送的检查频率
+poll_interval = 2.0
+

+ 46 - 19
sendmsg/send_email.py

@@ -25,7 +25,7 @@ from email.header import Header
 from email.mime.multipart import MIMEMultipart
 from email.mime.text import MIMEText
 from email.utils import formataddr
-from sqlalchemy import select
+from sqlalchemy import select, update
 
 # 加载配置文件
 config_file = Path(__file__).parent / "send.conf"
@@ -90,7 +90,7 @@ EMAIL_BODY_TEMPLATE = """
       <td align="center" style="padding:32px 12px;">
         <div class="wrap">
           <div class="container">
-            <div class="logo">Lyrria</div>
+            <div class="logo">Lumi</div>
             <div class="hero">
               <img src="https://assets.lyrria.com/Lumi-cap.png" alt="Lumi" />
             </div>
@@ -219,32 +219,59 @@ async def process_email_codes(db_manager: DatabaseManager):
                     SMTP_PASSWORD
                 )
                 
-                # 更新数据库状态
+                # 🔥 使用显式 UPDATE 语句更新数据库(确保数据正确保存)
+                now = datetime.now()
+                code_id = code_record.code_id
+                
                 if success:
-                    code_record.is_sent = True
-                    code_record.sent_at = datetime.now()
-                    code_record.metadata = code_record.metadata or {}
-                    code_record.metadata["send_status"] = "success"
-                    code_record.metadata["send_at"] = datetime.now().isoformat()
+                    # 成功:更新 is_sent、sent_at 和 metadata
+                    metadata_dict = code_record.metadata_json or {}
+                    metadata_dict["send_status"] = "success"
+                    metadata_dict["send_at"] = now.isoformat()
+                    
+                    await session.execute(
+                        update(VerificationCode)
+                        .where(VerificationCode.code_id == code_id)
+                        .values(
+                            is_sent=True,
+                            sent_at=now,
+                            metadata_json=metadata_dict
+                        )
+                    )
+                    await session.commit()
                     success_count += 1
                 else:
-                    code_record.metadata = code_record.metadata or {}
-                    code_record.metadata["send_status"] = "failed"
-                    code_record.metadata["error"] = error_msg
-                    code_record.metadata["send_at"] = datetime.now().isoformat()
+                    # 失败:只更新 metadata(不更新 is_sent 和 sent_at)
+                    metadata_dict = code_record.metadata_json or {}
+                    metadata_dict["send_status"] = "failed"
+                    metadata_dict["error"] = error_msg
+                    metadata_dict["send_at"] = now.isoformat()
+                    
+                    await session.execute(
+                        update(VerificationCode)
+                        .where(VerificationCode.code_id == code_id)
+                        .values(metadata_json=metadata_dict)
+                    )
+                    await session.commit()
                     fail_count += 1
                 
-                await session.commit()
-                
             except Exception as e:
                 logger.error(f"❌ 处理验证码失败: code_id={code_record.code_id}, error={e}")
                 try:
-                    code_record.metadata = code_record.metadata or {}
-                    code_record.metadata["send_status"] = "error"
-                    code_record.metadata["error"] = str(e)
+                    # 异常:更新 metadata 记录错误信息
+                    metadata_dict = code_record.metadata_json or {}
+                    metadata_dict["send_status"] = "error"
+                    metadata_dict["error"] = str(e)
+                    metadata_dict["send_at"] = datetime.now().isoformat()
+                    
+                    await session.execute(
+                        update(VerificationCode)
+                        .where(VerificationCode.code_id == code_record.code_id)
+                        .values(metadata_json=metadata_dict)
+                    )
                     await session.commit()
-                except:
-                    pass
+                except Exception as update_error:
+                    logger.error(f"❌ 更新错误状态失败: code_id={code_record.code_id}, error={update_error}")
                 fail_count += 1
         
         if success_count > 0 or fail_count > 0:

+ 328 - 0
sendmsg/send_sms_gn.py

@@ -0,0 +1,328 @@
+"""发送国内短信验证码服务(准实时)
+
+从数据库读取待发送的验证码(area_code=+86),使用阿里云短信服务发送。
+
+Prerequisites:
+- Python >= 3.7
+- `pip install alibabacloud_dysmsapi20170525==4.3.0`
+- 配置文件:send.conf(与脚本同目录)
+  - 必需配置:[aliyun_sms] 下的 access_key_id 和 access_key_secret
+  - 必需配置:[database] 下的数据库配置
+
+运行: python send_sms_gn.py
+"""
+
+import asyncio
+import configparser
+import os
+import sys
+import logging
+from datetime import datetime
+from pathlib import Path
+from typing import Dict, Optional, Tuple
+
+from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
+from alibabacloud_dysmsapi20170525 import models as dysms_models
+from alibabacloud_tea_openapi import models as open_api_models
+from alibabacloud_tea_util import models as util_models
+from alibabacloud_tea_util.client import Client as UtilClient
+from Tea.exceptions import UnretryableException, TeaException
+from sqlalchemy import select, update
+
+# 加载配置文件
+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()
+SIGN_NAME = config.get("aliyun_sms", "sign_name", fallback="灵语科技")
+TEMPLATE_CODE = config.get("aliyun_sms", "template_code", fallback="SMS_498720756")
+ENDPOINT = config.get("aliyun_sms", "endpoint", fallback="dysmsapi.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"))
+
+
+def create_client() -> Dysmsapi20170525Client:
+    """初始化阿里云短信客户端(从配置文件读取 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 file."
+        )
+
+    client_config = open_api_models.Config(
+        access_key_id=ACCESS_KEY_ID,
+        access_key_secret=ACCESS_KEY_SECRET,
+        endpoint=ENDPOINT,
+    )
+    return Dysmsapi20170525Client(client_config)
+
+
+async def send_sms_async(
+    client: Dysmsapi20170525Client,
+    phone: str,
+    code: str,
+) -> Tuple[bool, Optional[str]]:
+    """
+    发送短信验证码(异步包装)
+    
+    Args:
+        client: 阿里云短信客户端
+        phone: 手机号
+        code: 验证码
+    
+    Returns:
+        (是否成功, 错误信息)
+    """
+    template_params: Dict[str, str] = {"code": code}
+    
+    request = dysms_models.SendSmsRequest(
+        phone_numbers=phone,
+        sign_name=SIGN_NAME,
+        template_code=TEMPLATE_CODE,
+        template_param=UtilClient.to_jsonstring(template_params),
+    )
+
+    try:
+        # 使用 asyncio.to_thread 将同步调用转为异步
+        resp = await asyncio.to_thread(
+            client.send_sms_with_options,
+            request,
+            util_models.RuntimeOptions()
+        )
+        
+        # 检查响应
+        if resp.body.code == "OK":
+            logger.info(f"✅ 短信发送成功: phone={phone}, code_id={code[:4]}***")
+            return True, None
+        else:
+            error_msg = f"Code: {resp.body.code}, Message: {resp.body.message}"
+            logger.error(f"❌ 短信发送失败: phone={phone}, {error_msg}")
+            return False, error_msg
+            
+    except UnretryableException as e:
+        error_msg = f"Network error: {e}"
+        logger.error(f"❌ 短信发送网络错误: phone={phone}, {error_msg}")
+        return False, error_msg
+    except TeaException as e:
+        error_msg = f"Business error: {e.message}"
+        if e.data:
+            error_msg += f", Recommend: {e.data.get('Recommend', '')}"
+        logger.error(f"❌ 短信发送业务错误: phone={phone}, {error_msg}")
+        return False, error_msg
+    except Exception as e:
+        error_msg = f"Unexpected error: {e}"
+        logger.error(f"❌ 短信发送异常: phone={phone}, {error_msg}")
+        return False, error_msg
+
+
+async def process_domestic_sms_codes(client: Dysmsapi20170525Client, db_manager: DatabaseManager):
+    """扫描并发送国内短信验证码(area_code=+86)"""
+    async with db_manager.get_session() as session:
+        # 查询未发送的国内短信验证码(area_code=+86)
+        result = await session.execute(
+            select(VerificationCode)
+            .where(
+                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)  # 每次处理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:
+                # 发送短信
+                success, error_msg = await send_sms_async(
+                    client,
+                    code_record.phone,
+                    code_record.code
+                )
+                
+                # 更新数据库状态(使用显式 UPDATE,避免 ORM 同步问题)
+                now = datetime.now()
+                if success:
+                    await session.execute(
+                        update(VerificationCode)
+                        .where(VerificationCode.id == code_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 == code_record.id)
+                        .values(
+                            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"❌ 处理验证码失败: code_id={code_record.code_id}, error={e}")
+                try:
+                    now = datetime.now()
+                    await session.execute(
+                        update(VerificationCode)
+                        .where(VerificationCode.id == code_record.id)
+                        .values(
+                            metadata_json={
+                                **(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_domestic_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_sms_gn.log,错误日志输出到 send_sms_gn_error.log"""
+    log_dir = Path(__file__).parent
+    log_file = log_dir / "send_sms_gn.log"
+    error_log_file = log_dir / "send_sms_gn_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()
+    
+    # 1. 控制台处理器(INFO及以上)
+    console_handler = logging.StreamHandler(sys.stdout)
+    console_handler.setLevel(logging.INFO)
+    console_handler.setFormatter(formatter)
+    root_logger.addHandler(console_handler)
+    
+    # 2. 文件处理器(所有日志,INFO及以上)
+    file_handler = logging.FileHandler(log_file, encoding='utf-8')
+    file_handler.setLevel(logging.INFO)
+    file_handler.setFormatter(formatter)
+    root_logger.addHandler(file_handler)
+    
+    # 3. 错误日志文件处理器(ERROR及以上)
+    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())
+

+ 110 - 0
sendmsg/start_prod.sh

@@ -0,0 +1,110 @@
+#!/bin/bash
+
+# 生产环境启动脚本
+# 用于启动验证码发送服务(邮件/短信)
+
+set -e  # 遇到错误立即退出
+
+# 获取脚本所在目录
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+# 颜色输出
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# 服务类型:email 或 sms
+SERVICE_TYPE="${1:-all}"
+
+# 检查配置文件
+CONF_FILE="$SCRIPT_DIR/send.conf"
+if [ ! -f "$CONF_FILE" ]; then
+    echo -e "${RED}❌ 错误: 未找到配置文件 $CONF_FILE${NC}"
+    echo -e "${YELLOW}提示: 请确保 send.conf 文件存在于 sendmsg 目录${NC}"
+    exit 1
+fi
+
+echo -e "${GREEN}✅ 找到配置文件: $CONF_FILE${NC}"
+
+# 检查 Python 环境
+if ! command -v python3 &> /dev/null; then
+    echo -e "${RED}❌ 错误: 未找到 python3 命令${NC}"
+    exit 1
+fi
+
+# 检查虚拟环境(可选,如果使用虚拟环境)
+if [ -d "venv" ] || [ -d ".venv" ]; then
+    VENV_DIR=""
+    if [ -d "venv" ]; then
+        VENV_DIR="venv"
+    else
+        VENV_DIR=".venv"
+    fi
+    
+    echo -e "${GREEN}✅ 激活虚拟环境: $VENV_DIR${NC}"
+    source "$VENV_DIR/bin/activate"
+fi
+
+# 检查依赖是否已安装
+if ! python3 -c "import sqlalchemy" &> /dev/null; then
+    echo -e "${YELLOW}⚠️  警告: 未检测到 SQLAlchemy,请确保已安装依赖${NC}"
+    echo -e "${YELLOW}提示: 可能需要安装 backend 目录的依赖${NC}"
+fi
+
+# 启动服务的函数
+start_email_service() {
+    echo -e "${BLUE}📧 启动邮件验证码发送服务...${NC}"
+    nohup python3 send_email.py > send_email_nohup.log 2>&1 &
+    EMAIL_PID=$!
+    echo $EMAIL_PID > send_email.pid
+    echo -e "${GREEN}✅ 邮件服务已启动,PID: $EMAIL_PID${NC}"
+    echo -e "${YELLOW}日志文件: send_email.log, send_email_error.log${NC}"
+    echo -e "${YELLOW}后台日志: send_email_nohup.log${NC}"
+}
+
+start_sms_service() {
+    echo -e "${BLUE}📱 启动短信验证码发送服务...${NC}"
+    nohup python3 send_sms_gn.py > send_sms_gn_nohup.log 2>&1 &
+    SMS_PID=$!
+    echo $SMS_PID > send_sms_gn.pid
+    echo -e "${GREEN}✅ 短信服务已启动,PID: $SMS_PID${NC}"
+    echo -e "${YELLOW}日志文件: send_sms_gn.log, send_sms_gn_error.log${NC}"
+    echo -e "${YELLOW}后台日志: send_sms_gn_nohup.log${NC}"
+}
+
+# 根据参数启动相应服务
+case "$SERVICE_TYPE" in
+    email)
+        start_email_service
+        ;;
+    sms)
+        start_sms_service
+        ;;
+    all)
+        start_email_service
+        sleep 1
+        start_sms_service
+        echo ""
+        echo -e "${GREEN}✅ 所有服务已启动${NC}"
+        echo -e "${YELLOW}查看进程: ps aux | grep -E 'send_email|send_sms_gn'${NC}"
+        echo -e "${YELLOW}停止服务: ./stop_prod.sh${NC}"
+        ;;
+    *)
+        echo -e "${RED}❌ 错误: 未知的服务类型 '$SERVICE_TYPE'${NC}"
+        echo ""
+        echo "用法: $0 [email|sms|all]"
+        echo ""
+        echo "参数说明:"
+        echo "  email  - 仅启动邮件验证码发送服务"
+        echo "  sms    - 仅启动短信验证码发送服务"
+        echo "  all    - 启动所有服务(默认)"
+        exit 1
+        ;;
+esac
+
+echo ""
+echo -e "${GREEN}🚀 服务启动完成!${NC}"
+

+ 92 - 0
sendmsg/status_prod.sh

@@ -0,0 +1,92 @@
+#!/bin/bash
+
+# 生产环境服务状态查看脚本
+# 用于查看验证码发送服务的运行状态
+
+# 获取脚本所在目录
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+# 颜色输出
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+echo -e "${BLUE}📊 验证码发送服务状态${NC}"
+echo "=================================="
+echo ""
+
+# 检查邮件服务
+check_email_service() {
+    PID_FILE="$SCRIPT_DIR/send_email.pid"
+    if [ -f "$PID_FILE" ]; then
+        PID=$(cat "$PID_FILE")
+        if ps -p "$PID" > /dev/null 2>&1; then
+            echo -e "${GREEN}✅ 邮件服务运行中${NC}"
+            echo "  PID: $PID"
+            ps -p "$PID" -o pid,etime,cmd --no-headers | awk '{print "  运行时间: " $2}'
+        else
+            echo -e "${RED}❌ 邮件服务未运行 (PID 文件存在但进程不存在)${NC}"
+        fi
+    else
+        # 尝试通过进程名查找
+        PIDS=$(pgrep -f "send_email.py" || true)
+        if [ -n "$PIDS" ]; then
+            echo -e "${YELLOW}⚠️  邮件服务运行中 (但未找到 PID 文件)${NC}"
+            echo "  PIDs: $PIDS"
+        else
+            echo -e "${RED}❌ 邮件服务未运行${NC}"
+        fi
+    fi
+    
+    # 显示日志文件信息
+    if [ -f "$SCRIPT_DIR/send_email.log" ]; then
+        LOG_SIZE=$(ls -lh "$SCRIPT_DIR/send_email.log" | awk '{print $5}')
+        LOG_TIME=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$SCRIPT_DIR/send_email.log" 2>/dev/null || stat -c "%y" "$SCRIPT_DIR/send_email.log" 2>/dev/null | cut -d. -f1)
+        echo "  日志文件: send_email.log ($LOG_SIZE, 最后更新: $LOG_TIME)"
+    fi
+    echo ""
+}
+
+# 检查短信服务
+check_sms_service() {
+    PID_FILE="$SCRIPT_DIR/send_sms_gn.pid"
+    if [ -f "$PID_FILE" ]; then
+        PID=$(cat "$PID_FILE")
+        if ps -p "$PID" > /dev/null 2>&1; then
+            echo -e "${GREEN}✅ 短信服务运行中${NC}"
+            echo "  PID: $PID"
+            ps -p "$PID" -o pid,etime,cmd --no-headers | awk '{print "  运行时间: " $2}'
+        else
+            echo -e "${RED}❌ 短信服务未运行 (PID 文件存在但进程不存在)${NC}"
+        fi
+    else
+        # 尝试通过进程名查找
+        PIDS=$(pgrep -f "send_sms_gn.py" || true)
+        if [ -n "$PIDS" ]; then
+            echo -e "${YELLOW}⚠️  短信服务运行中 (但未找到 PID 文件)${NC}"
+            echo "  PIDs: $PIDS"
+        else
+            echo -e "${RED}❌ 短信服务未运行${NC}"
+        fi
+    fi
+    
+    # 显示日志文件信息
+    if [ -f "$SCRIPT_DIR/send_sms_gn.log" ]; then
+        LOG_SIZE=$(ls -lh "$SCRIPT_DIR/send_sms_gn.log" | awk '{print $5}')
+        LOG_TIME=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$SCRIPT_DIR/send_sms_gn.log" 2>/dev/null || stat -c "%y" "$SCRIPT_DIR/send_sms_gn.log" 2>/dev/null | cut -d. -f1)
+        echo "  日志文件: send_sms_gn.log ($LOG_SIZE, 最后更新: $LOG_TIME)"
+    fi
+    echo ""
+}
+
+check_email_service
+check_sms_service
+
+echo "=================================="
+echo -e "${BLUE}💡 提示:${NC}"
+echo "  启动服务: ./start_prod.sh [email|sms|all]"
+echo "  停止服务: ./stop_prod.sh [email|sms|all]"
+echo "  查看日志: tail -f send_email.log 或 tail -f send_sms_gn.log"
+

+ 129 - 0
sendmsg/stop_prod.sh

@@ -0,0 +1,129 @@
+#!/bin/bash
+
+# 生产环境停止脚本
+# 用于停止验证码发送服务(邮件/短信)
+
+set -e  # 遇到错误立即退出
+
+# 获取脚本所在目录
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+# 颜色输出
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+NC='\033[0m' # No Color
+
+# 服务类型:email 或 sms 或 all
+SERVICE_TYPE="${1:-all}"
+
+# 停止服务的函数
+stop_email_service() {
+    PID_FILE="$SCRIPT_DIR/send_email.pid"
+    if [ -f "$PID_FILE" ]; then
+        PID=$(cat "$PID_FILE")
+        if ps -p "$PID" > /dev/null 2>&1; then
+            echo -e "${YELLOW}🛑 正在停止邮件服务 (PID: $PID)...${NC}"
+            kill "$PID"
+            # 等待进程结束
+            for i in {1..10}; do
+                if ! ps -p "$PID" > /dev/null 2>&1; then
+                    break
+                fi
+                sleep 1
+            done
+            # 如果还在运行,强制杀死
+            if ps -p "$PID" > /dev/null 2>&1; then
+                echo -e "${YELLOW}⚠️  强制停止邮件服务...${NC}"
+                kill -9 "$PID" 2>/dev/null || true
+            fi
+            rm -f "$PID_FILE"
+            echo -e "${GREEN}✅ 邮件服务已停止${NC}"
+        else
+            echo -e "${YELLOW}⚠️  邮件服务进程不存在 (PID: $PID)${NC}"
+            rm -f "$PID_FILE"
+        fi
+    else
+        # 尝试通过进程名查找
+        PIDS=$(pgrep -f "send_email.py" || true)
+        if [ -n "$PIDS" ]; then
+            echo -e "${YELLOW}🛑 正在停止邮件服务 (通过进程名查找)...${NC}"
+            echo "$PIDS" | xargs kill 2>/dev/null || true
+            sleep 2
+            echo "$PIDS" | xargs kill -9 2>/dev/null || true
+            echo -e "${GREEN}✅ 邮件服务已停止${NC}"
+        else
+            echo -e "${YELLOW}⚠️  未找到运行中的邮件服务${NC}"
+        fi
+    fi
+}
+
+stop_sms_service() {
+    PID_FILE="$SCRIPT_DIR/send_sms_gn.pid"
+    if [ -f "$PID_FILE" ]; then
+        PID=$(cat "$PID_FILE")
+        if ps -p "$PID" > /dev/null 2>&1; then
+            echo -e "${YELLOW}🛑 正在停止短信服务 (PID: $PID)...${NC}"
+            kill "$PID"
+            # 等待进程结束
+            for i in {1..10}; do
+                if ! ps -p "$PID" > /dev/null 2>&1; then
+                    break
+                fi
+                sleep 1
+            done
+            # 如果还在运行,强制杀死
+            if ps -p "$PID" > /dev/null 2>&1; then
+                echo -e "${YELLOW}⚠️  强制停止短信服务...${NC}"
+                kill -9 "$PID" 2>/dev/null || true
+            fi
+            rm -f "$PID_FILE"
+            echo -e "${GREEN}✅ 短信服务已停止${NC}"
+        else
+            echo -e "${YELLOW}⚠️  短信服务进程不存在 (PID: $PID)${NC}"
+            rm -f "$PID_FILE"
+        fi
+    else
+        # 尝试通过进程名查找
+        PIDS=$(pgrep -f "send_sms_gn.py" || true)
+        if [ -n "$PIDS" ]; then
+            echo -e "${YELLOW}🛑 正在停止短信服务 (通过进程名查找)...${NC}"
+            echo "$PIDS" | xargs kill 2>/dev/null || true
+            sleep 2
+            echo "$PIDS" | xargs kill -9 2>/dev/null || true
+            echo -e "${GREEN}✅ 短信服务已停止${NC}"
+        else
+            echo -e "${YELLOW}⚠️  未找到运行中的短信服务${NC}"
+        fi
+    fi
+}
+
+# 根据参数停止相应服务
+case "$SERVICE_TYPE" in
+    email)
+        stop_email_service
+        ;;
+    sms)
+        stop_sms_service
+        ;;
+    all)
+        stop_email_service
+        sleep 1
+        stop_sms_service
+        echo ""
+        echo -e "${GREEN}✅ 所有服务已停止${NC}"
+        ;;
+    *)
+        echo -e "${RED}❌ 错误: 未知的服务类型 '$SERVICE_TYPE'${NC}"
+        echo ""
+        echo "用法: $0 [email|sms|all]"
+        echo ""
+        echo "参数说明:"
+        echo "  email  - 仅停止邮件验证码发送服务"
+        echo "  sms    - 仅停止短信验证码发送服务"
+        echo "  all    - 停止所有服务(默认)"
+        exit 1
+        ;;
+esac
+