diff --git a/api/.env.production b/api/.env.production index acc0061..246982e 100644 --- a/api/.env.production +++ b/api/.env.production @@ -38,8 +38,12 @@ ACCESS_TOKEN_EXPIRE_MINUTES=120 TENCENT_SMS_SECRET_ID=AKIDmXEoYQCypUyFI9nH7tWzGOJG7e3ING0Y TENCENT_SMS_SECRET_KEY=Vq22tb7UghhV6WnskRyQjroY5QDqHUXO # 短信应用 SDK AppID -TENCENT_SMS_SDK_APP_ID=1319381411 +TENCENT_SMS_SDK_APP_ID=1401010099 # 短信签名内容(不包含【】符号) TENCENT_SMS_SIGN_NAME=上海华嘎科技有限公司 # 短信模板 ID -TENCENT_SMS_TEMPLATE_ID=2592163 \ No newline at end of file +TENCENT_SMS_TEMPLATE_ID=2592163 +# 短信模板参数数量(1=仅验证码,2=验证码+过期时间) +# 如果遇到 TemplateParamSetNotMatchApprovedTemplate 错误,请检查腾讯云控制台中的模板配置 +# 并根据实际模板参数数量设置此值 +TENCENT_SMS_TEMPLATE_PARAM_COUNT=1 \ No newline at end of file diff --git a/api/services/sms_service.py b/api/services/sms_service.py index 7004a9b..769067e 100644 --- a/api/services/sms_service.py +++ b/api/services/sms_service.py @@ -22,6 +22,8 @@ TENCENT_SMS_SDK_APP_ID = os.getenv("TENCENT_SMS_SDK_APP_ID", "") TENCENT_SMS_SIGN_NAME = os.getenv("TENCENT_SMS_SIGN_NAME", "") # 统一使用一个短信模板ID(所有场景共用) TENCENT_SMS_TEMPLATE_ID = os.getenv("TENCENT_SMS_TEMPLATE_ID", "") +# 模板参数配置:1=仅验证码,2=验证码+过期时间(默认2) +TENCENT_SMS_TEMPLATE_PARAM_COUNT = int(os.getenv("TENCENT_SMS_TEMPLATE_PARAM_COUNT", "2")) # 验证码配置 CODE_LENGTH = 6 # 验证码长度 @@ -39,7 +41,7 @@ def get_template_id_by_purpose(purpose: str) -> str: return TENCENT_SMS_TEMPLATE_ID -def send_sms_via_tencent(phone: str, code: str, purpose: str) -> bool: +def send_sms_via_tencent(phone: str, code: str, purpose: str) -> Tuple[bool, str]: """ 通过腾讯云发送短信验证码 @@ -49,43 +51,103 @@ def send_sms_via_tencent(phone: str, code: str, purpose: str) -> bool: purpose: 用途 Returns: - bool: 是否发送成功 + Tuple[bool, str]: (是否发送成功, 错误消息) """ - try: - # 创建认证对象 - cred = credential.Credential(TENCENT_SMS_SECRET_ID, TENCENT_SMS_SECRET_KEY) - - # 创建SMS客户端 - client = sms_client.SmsClient(cred, "ap-guangzhou") - - # 创建请求对象 - req = sms_models.SendSmsRequest() - req.SmsSdkAppId = TENCENT_SMS_SDK_APP_ID - req.SignName = TENCENT_SMS_SIGN_NAME - req.TemplateId = get_template_id_by_purpose(purpose) - req.TemplateParamSet = [code, str(CODE_EXPIRE_MINUTES)] # 验证码和过期时间 - req.PhoneNumberSet = [f"+86{phone}"] - - # 发送短信 - resp = client.SendSms(req) - - # 检查发送结果 - if resp.SendStatusSet and len(resp.SendStatusSet) > 0: - status = resp.SendStatusSet[0] - if status.Code == "Ok": - return True - else: - print(f"短信发送失败: {status.Code} - {status.Message}") - return False - - return False - - except TencentCloudSDKException as e: - print(f"腾讯云SDK异常: {e}") - return False - except Exception as e: - print(f"发送短信异常: {e}") - return False + # 检查配置是否完整 + if not TENCENT_SMS_SECRET_ID or not TENCENT_SMS_SECRET_KEY: + error_msg = "腾讯云短信服务配置不完整,请设置 TENCENT_SMS_SECRET_ID 和 TENCENT_SMS_SECRET_KEY 环境变量" + print(f"错误: {error_msg}") + return False, error_msg + + if not TENCENT_SMS_SDK_APP_ID or not TENCENT_SMS_SIGN_NAME or not TENCENT_SMS_TEMPLATE_ID: + error_msg = "腾讯云短信服务配置不完整,请设置 TENCENT_SMS_SDK_APP_ID、TENCENT_SMS_SIGN_NAME 和 TENCENT_SMS_TEMPLATE_ID 环境变量" + print(f"错误: {error_msg}") + return False, error_msg + + # 创建认证对象和客户端(复用) + cred = credential.Credential(TENCENT_SMS_SECRET_ID, TENCENT_SMS_SECRET_KEY) + client = sms_client.SmsClient(cred, "ap-guangzhou") + + # 尝试发送短信,如果参数不匹配则自动重试另一种配置 + param_configs = [ + (1, [code]), # 单参数:仅验证码 + (2, [code, str(CODE_EXPIRE_MINUTES)]) # 双参数:验证码+过期时间 + ] + + # 优先使用配置的参数数量 + if TENCENT_SMS_TEMPLATE_PARAM_COUNT == 1: + param_configs = [param_configs[0], param_configs[1]] # 先试单参数,再试双参数 + else: + param_configs = [param_configs[1], param_configs[0]] # 先试双参数,再试单参数 + + last_error = None + for param_count, template_params in param_configs: + try: + # 创建请求对象 + req = sms_models.SendSmsRequest() + req.SmsSdkAppId = TENCENT_SMS_SDK_APP_ID + req.SignName = TENCENT_SMS_SIGN_NAME + req.TemplateId = get_template_id_by_purpose(purpose) + req.TemplateParamSet = template_params + req.PhoneNumberSet = [f"+86{phone}"] + + print(f"调试: 尝试发送短信 - 模板ID={req.TemplateId}, 参数数量={param_count}, 参数={template_params}") + + # 发送短信 + resp = client.SendSms(req) + + # 检查发送结果 + if resp.SendStatusSet and len(resp.SendStatusSet) > 0: + status = resp.SendStatusSet[0] + if status.Code == "Ok": + print(f"调试: 短信发送成功,使用的参数数量={param_count}") + return True, "" + else: + error_msg = f"短信发送失败: {status.Code} - {status.Message}" + print(f"调试: {error_msg}") + last_error = error_msg + + # 如果是参数不匹配错误,尝试下一个配置 + if "TemplateParamSetNotMatchApprovedTemplate" in status.Code or "FailedOperation.TemplateParamSetNotMatchApprovedTemplate" in status.Code: + print(f"调试: 参数不匹配,尝试下一个配置...") + continue + else: + return False, error_msg + + last_error = "短信发送失败: 未收到有效响应" + print(f"调试: {last_error}") + + except TencentCloudSDKException as e: + error_msg = f"腾讯云SDK异常: {e}" + print(f"调试: {error_msg}") + last_error = error_msg + + # 如果是参数不匹配错误,尝试下一个配置 + if "TemplateParamSetNotMatchApprovedTemplate" in str(e) or "FailedOperation.TemplateParamSetNotMatchApprovedTemplate" in str(e): + print(f"调试: 参数不匹配,尝试下一个配置...") + continue + + # 其他错误直接返回 + if "SmsSdkAppIdVerifyFail" in str(e) or "UnauthorizedOperation.SmsSdkAppIdVerifyFail" in str(e): + error_msg = "短信服务配置错误: SmsSdkAppId 验证失败,请检查 TENCENT_SMS_SDK_APP_ID 是否正确,以及该 AppId 是否属于当前 API 密钥对应的账户" + elif "InvalidCredential" in str(e) or "secret id should not be none" in str(e).lower(): + error_msg = "短信服务配置错误: API 密钥无效,请检查 TENCENT_SMS_SECRET_ID 和 TENCENT_SMS_SECRET_KEY 是否正确" + elif "UnauthorizedOperation" in str(e): + error_msg = f"短信服务授权失败: {e.message if hasattr(e, 'message') else str(e)}" + + return False, error_msg + except Exception as e: + error_msg = f"发送短信异常: {str(e)}" + print(f"调试: {error_msg}") + return False, error_msg + + # 所有配置都失败了 + if "TemplateParamSetNotMatchApprovedTemplate" in str(last_error) or "FailedOperation.TemplateParamSetNotMatchApprovedTemplate" in str(last_error): + error_msg = f"短信模板参数不匹配: 已尝试单参数和双参数配置,均失败。请检查腾讯云控制台中的模板配置(模板ID: {get_template_id_by_purpose(purpose)}),确认模板实际需要的参数数量和格式,然后设置正确的 TENCENT_SMS_TEMPLATE_PARAM_COUNT 环境变量(1=仅验证码,2=验证码+过期时间)" + else: + error_msg = last_error or "短信发送失败" + + return False, error_msg async def check_rate_limit(db: AsyncSession, phone: str) -> Tuple[bool, int]: @@ -149,10 +211,13 @@ async def send_verification_code( code = generate_verification_code() # 发送短信 - success = send_sms_via_tencent(phone, code, purpose) + success, error_msg = send_sms_via_tencent(phone, code, purpose) if not success: - return False, "短信发送失败,请稍后重试", 0 + # 如果错误消息为空,使用默认消息 + if not error_msg: + error_msg = "短信发送失败,请稍后重试" + return False, error_msg, 0 # 保存到数据库 expires_at = utc_now() + timedelta(minutes=CODE_EXPIRE_MINUTES)