Add Aliyun SMS settings and refresh admin login
This commit is contained in:
@@ -233,6 +233,54 @@ const systemSettingSections: SystemSettingSection[] = [
|
|||||||
placeholder: "例如 123456",
|
placeholder: "例如 123456",
|
||||||
description: "mock 短信开启时使用的固定验证码。",
|
description: "mock 短信开启时使用的固定验证码。",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "aliyun_sms_access_key_id",
|
||||||
|
label: "阿里云 AccessKey ID",
|
||||||
|
type: "password",
|
||||||
|
defaultValue: "",
|
||||||
|
placeholder: "请输入 RAM 用户 AccessKey ID",
|
||||||
|
description: "关闭 mock 短信后用于调用阿里云短信服务。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "aliyun_sms_access_key_secret",
|
||||||
|
label: "阿里云 AccessKey Secret",
|
||||||
|
type: "password",
|
||||||
|
defaultValue: "",
|
||||||
|
placeholder: "请输入 RAM 用户 AccessKey Secret",
|
||||||
|
description: "敏感配置,建议使用最小权限 RAM 用户。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "aliyun_sms_sign_name",
|
||||||
|
label: "阿里云短信签名",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "",
|
||||||
|
placeholder: "例如 某某课堂",
|
||||||
|
description: "需与阿里云短信控制台审核通过的签名一致。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "aliyun_sms_template_code",
|
||||||
|
label: "阿里云模板 Code",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "",
|
||||||
|
placeholder: "例如 SMS_123456789",
|
||||||
|
description: "验证码短信模板 Code。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "aliyun_sms_template_param_key",
|
||||||
|
label: "验证码变量名",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "code",
|
||||||
|
placeholder: "默认 code",
|
||||||
|
description: "模板中验证码变量名,例如模板变量为 ${code} 时填写 code。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "aliyun_sms_endpoint",
|
||||||
|
label: "阿里云短信 Endpoint",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "dysmsapi.aliyuncs.com",
|
||||||
|
placeholder: "dysmsapi.aliyuncs.com",
|
||||||
|
description: "一般保持默认值即可。",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "default_user_name_prefix",
|
key: "default_user_name_prefix",
|
||||||
label: "默认用户名前缀",
|
label: "默认用户名前缀",
|
||||||
@@ -984,19 +1032,41 @@ function buildChatQuery() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main v-if="!admin" class="login-page">
|
<main v-if="!admin" class="login-page">
|
||||||
<section class="login-box">
|
<section class="login-hero">
|
||||||
<div class="brand">AI</div>
|
<div class="login-hero-main">
|
||||||
<h1>AI 知识库后台</h1>
|
<div class="login-brand">
|
||||||
<p>用于维护用户、知识库、Agent、模型和审计记录。</p>
|
<span>AI</span>
|
||||||
<el-form label-position="top" @submit.prevent="login">
|
<strong>知识库管理后台</strong>
|
||||||
<el-form-item label="管理员账号">
|
</div>
|
||||||
<el-input v-model="loginForm.username" />
|
<h1>让知识库、Agent 和用户运营保持在同一个控制台里。</h1>
|
||||||
</el-form-item>
|
<p>统一维护学员、知识库、模型配置、问答记录和系统运行参数,方便排查与交接。</p>
|
||||||
<el-form-item label="密码">
|
<div class="login-highlights">
|
||||||
<el-input v-model="loginForm.password" type="password" show-password />
|
<span>知识库运营</span>
|
||||||
</el-form-item>
|
<span>Agent 调试</span>
|
||||||
<el-button type="primary" :loading="loading" class="full-btn" @click="login">登录</el-button>
|
<span>记录审计</span>
|
||||||
</el-form>
|
</div>
|
||||||
|
</div>
|
||||||
|
<section class="login-card">
|
||||||
|
<div class="login-card-head">
|
||||||
|
<h2>管理员登录</h2>
|
||||||
|
<p>请输入后台管理员账号和密码。</p>
|
||||||
|
</div>
|
||||||
|
<el-form label-position="top" @submit.prevent="login">
|
||||||
|
<el-form-item label="管理员账号">
|
||||||
|
<el-input v-model="loginForm.username" size="large" placeholder="请输入管理员账号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.password"
|
||||||
|
size="large"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-button type="primary" :loading="loading" class="full-btn login-submit" @click="login">登录后台</el-button>
|
||||||
|
</el-form>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -23,32 +23,146 @@ textarea {
|
|||||||
|
|
||||||
.login-page {
|
.login-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: grid;
|
display: flex;
|
||||||
place-items: center;
|
align-items: center;
|
||||||
background: #eef4f2;
|
justify-content: center;
|
||||||
|
padding: 48px;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(15, 115, 93, 0.14), rgba(234, 179, 8, 0.08)),
|
||||||
|
#f4f7f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-box {
|
.login-hero {
|
||||||
width: 380px;
|
width: min(1080px, 100%);
|
||||||
padding: 32px;
|
min-height: 620px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.1fr) 420px;
|
||||||
|
overflow: hidden;
|
||||||
border: 1px solid #dce6e2;
|
border: 1px solid #dce6e2;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
box-shadow: 0 18px 44px rgba(35, 54, 49, 0.08);
|
box-shadow: 0 24px 70px rgba(31, 45, 42, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-hero-main {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 48px;
|
||||||
|
background: #12352e;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hero-main::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: 42px;
|
||||||
|
bottom: 42px;
|
||||||
|
width: 280px;
|
||||||
|
height: 280px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand span,
|
||||||
.brand {
|
.brand {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
margin-bottom: 18px;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #0f8b6f;
|
background: #0f8b6f;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-brand span {
|
||||||
|
background: #eab308;
|
||||||
|
color: #17231f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand strong {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hero-main h1 {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: 620px;
|
||||||
|
margin: 72px 0 0;
|
||||||
|
font-size: 40px;
|
||||||
|
line-height: 1.18;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hero-main p {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: 560px;
|
||||||
|
margin: 18px 0 0;
|
||||||
|
color: rgba(255, 255, 255, 0.74);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-highlights {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-highlights span {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.82);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card-head {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #132822;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1.25;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #667a73;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-submit {
|
||||||
|
height: 44px;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.login-box h1,
|
.login-box h1,
|
||||||
.page-head h2 {
|
.page-head h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -809,9 +923,33 @@ textarea {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-box {
|
.login-page {
|
||||||
width: min(380px, calc(100vw - 32px));
|
padding: 18px;
|
||||||
padding: 24px;
|
}
|
||||||
|
|
||||||
|
.login-hero {
|
||||||
|
min-height: auto;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hero-main {
|
||||||
|
padding: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hero-main::after {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
right: 18px;
|
||||||
|
bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hero-main h1 {
|
||||||
|
margin-top: 42px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
padding: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-shell {
|
.admin-shell {
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ ACCESS_TOKEN_EXPIRE_MINUTES=43200
|
|||||||
MOCK_SMS_ENABLED=true
|
MOCK_SMS_ENABLED=true
|
||||||
MOCK_SMS_CODE=123456
|
MOCK_SMS_CODE=123456
|
||||||
SMS_CODE_EXPIRE_MINUTES=5
|
SMS_CODE_EXPIRE_MINUTES=5
|
||||||
|
ALIYUN_SMS_ACCESS_KEY_ID=
|
||||||
|
ALIYUN_SMS_ACCESS_KEY_SECRET=
|
||||||
|
ALIYUN_SMS_SIGN_NAME=
|
||||||
|
ALIYUN_SMS_TEMPLATE_CODE=
|
||||||
|
ALIYUN_SMS_TEMPLATE_PARAM_KEY=code
|
||||||
|
ALIYUN_SMS_ENDPOINT=dysmsapi.aliyuncs.com
|
||||||
MOCK_RAG_ENABLED=false
|
MOCK_RAG_ENABLED=false
|
||||||
MOCK_MODEL_ENABLED=true
|
MOCK_MODEL_ENABLED=true
|
||||||
FEISHU_MOCK_ENABLED=false
|
FEISHU_MOCK_ENABLED=false
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ class Settings(BaseSettings):
|
|||||||
mock_sms_enabled: bool = True
|
mock_sms_enabled: bool = True
|
||||||
mock_sms_code: str = "123456"
|
mock_sms_code: str = "123456"
|
||||||
sms_code_expire_minutes: int = 5
|
sms_code_expire_minutes: int = 5
|
||||||
|
aliyun_sms_access_key_id: str = ""
|
||||||
|
aliyun_sms_access_key_secret: str = ""
|
||||||
|
aliyun_sms_sign_name: str = ""
|
||||||
|
aliyun_sms_template_code: str = ""
|
||||||
|
aliyun_sms_template_param_key: str = "code"
|
||||||
|
aliyun_sms_endpoint: str = "dysmsapi.aliyuncs.com"
|
||||||
mock_rag_enabled: bool = False
|
mock_rag_enabled: bool = False
|
||||||
mock_model_enabled: bool = True
|
mock_model_enabled: bool = True
|
||||||
feishu_mock_enabled: bool = False
|
feishu_mock_enabled: bool = False
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ class AuthService:
|
|||||||
def send_sms_code(cls, db: Session, phone: str) -> None:
|
def send_sms_code(cls, db: Session, phone: str) -> None:
|
||||||
user = cls._get_existing_user(db, phone)
|
user = cls._get_existing_user(db, phone)
|
||||||
cls._ensure_user_can_login(user)
|
cls._ensure_user_can_login(user)
|
||||||
SmsCodeService.send_code(phone)
|
SmsCodeService.send_code(db, phone)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def login_with_sms(cls, db: Session, phone: str, code: str) -> dict:
|
def login_with_sms(cls, db: Session, phone: str, code: str) -> dict:
|
||||||
SmsCodeService.verify_code(phone, code)
|
SmsCodeService.verify_code(db, phone, code)
|
||||||
user = cls._get_existing_user(db, phone)
|
user = cls._get_existing_user(db, phone)
|
||||||
cls._ensure_user_can_login(user)
|
cls._ensure_user_can_login(user)
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,19 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
from re import fullmatch
|
from re import fullmatch
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
|
from app.models.ai_config import SystemConfig
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -15,26 +23,40 @@ class SmsCodeRecord:
|
|||||||
expires_at: datetime
|
expires_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SmsProviderConfig:
|
||||||
|
mock_sms_enabled: bool
|
||||||
|
mock_sms_code: str
|
||||||
|
sms_code_expire_minutes: int
|
||||||
|
aliyun_sms_access_key_id: str
|
||||||
|
aliyun_sms_access_key_secret: str
|
||||||
|
aliyun_sms_sign_name: str
|
||||||
|
aliyun_sms_template_code: str
|
||||||
|
aliyun_sms_template_param_key: str
|
||||||
|
aliyun_sms_endpoint: str
|
||||||
|
|
||||||
|
|
||||||
class SmsCodeService:
|
class SmsCodeService:
|
||||||
_codes: dict[str, SmsCodeRecord] = {}
|
_codes: dict[str, SmsCodeRecord] = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def send_code(cls, phone: str) -> None:
|
def send_code(cls, db: Session, phone: str) -> None:
|
||||||
cls._validate_phone(phone)
|
cls._validate_phone(phone)
|
||||||
settings = get_settings()
|
config = _load_sms_config(db)
|
||||||
if not settings.mock_sms_enabled:
|
code = config.mock_sms_code if config.mock_sms_enabled else _generate_sms_code()
|
||||||
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="真实短信服务尚未接入")
|
if not config.mock_sms_enabled:
|
||||||
|
_send_aliyun_sms(phone, code, config)
|
||||||
cls._codes[phone] = SmsCodeRecord(
|
cls._codes[phone] = SmsCodeRecord(
|
||||||
code=settings.mock_sms_code,
|
code=code,
|
||||||
expires_at=datetime.now(UTC) + timedelta(minutes=settings.sms_code_expire_minutes),
|
expires_at=datetime.now(UTC) + timedelta(minutes=config.sms_code_expire_minutes),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def verify_code(cls, phone: str, code: str) -> None:
|
def verify_code(cls, db: Session, phone: str, code: str) -> None:
|
||||||
cls._validate_phone(phone)
|
cls._validate_phone(phone)
|
||||||
record = cls._codes.get(phone)
|
record = cls._codes.get(phone)
|
||||||
settings = get_settings()
|
config = _load_sms_config(db)
|
||||||
if settings.mock_sms_enabled and record is None and code == settings.mock_sms_code:
|
if config.mock_sms_enabled and record is None and code == config.mock_sms_code:
|
||||||
return
|
return
|
||||||
if record is None:
|
if record is None:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码不存在或已过期")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码不存在或已过期")
|
||||||
@@ -49,3 +71,135 @@ class SmsCodeService:
|
|||||||
def _validate_phone(phone: str) -> None:
|
def _validate_phone(phone: str) -> None:
|
||||||
if fullmatch(r"1[3-9]\d{9}", phone) is None:
|
if fullmatch(r"1[3-9]\d{9}", phone) is None:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="手机号格式不正确")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="手机号格式不正确")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_sms_config(db: Session) -> SmsProviderConfig:
|
||||||
|
settings = get_settings()
|
||||||
|
rows = db.scalars(select(SystemConfig)).all()
|
||||||
|
values = {row.config_key: row.config_value for row in rows}
|
||||||
|
return SmsProviderConfig(
|
||||||
|
mock_sms_enabled=_config_bool(values, "mock_sms_enabled", settings.mock_sms_enabled),
|
||||||
|
mock_sms_code=_config_text(values, "mock_sms_code", settings.mock_sms_code),
|
||||||
|
sms_code_expire_minutes=_config_int(
|
||||||
|
values,
|
||||||
|
"sms_code_expire_minutes",
|
||||||
|
settings.sms_code_expire_minutes,
|
||||||
|
minimum=1,
|
||||||
|
maximum=60,
|
||||||
|
),
|
||||||
|
aliyun_sms_access_key_id=_config_text(
|
||||||
|
values,
|
||||||
|
"aliyun_sms_access_key_id",
|
||||||
|
settings.aliyun_sms_access_key_id,
|
||||||
|
),
|
||||||
|
aliyun_sms_access_key_secret=_config_text(
|
||||||
|
values,
|
||||||
|
"aliyun_sms_access_key_secret",
|
||||||
|
settings.aliyun_sms_access_key_secret,
|
||||||
|
),
|
||||||
|
aliyun_sms_sign_name=_config_text(values, "aliyun_sms_sign_name", settings.aliyun_sms_sign_name),
|
||||||
|
aliyun_sms_template_code=_config_text(
|
||||||
|
values,
|
||||||
|
"aliyun_sms_template_code",
|
||||||
|
settings.aliyun_sms_template_code,
|
||||||
|
),
|
||||||
|
aliyun_sms_template_param_key=_config_text(
|
||||||
|
values,
|
||||||
|
"aliyun_sms_template_param_key",
|
||||||
|
settings.aliyun_sms_template_param_key,
|
||||||
|
)
|
||||||
|
or "code",
|
||||||
|
aliyun_sms_endpoint=_config_text(values, "aliyun_sms_endpoint", settings.aliyun_sms_endpoint)
|
||||||
|
or "dysmsapi.aliyuncs.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_aliyun_sms(phone: str, code: str, config: SmsProviderConfig) -> None:
|
||||||
|
missing = [
|
||||||
|
name
|
||||||
|
for name, value in (
|
||||||
|
("AccessKey ID", config.aliyun_sms_access_key_id),
|
||||||
|
("AccessKey Secret", config.aliyun_sms_access_key_secret),
|
||||||
|
("短信签名", config.aliyun_sms_sign_name),
|
||||||
|
("模板 Code", config.aliyun_sms_template_code),
|
||||||
|
)
|
||||||
|
if not value
|
||||||
|
]
|
||||||
|
if missing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"阿里云短信配置不完整:请补充{','.join(missing)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from alibabacloud_dysmsapi20170525.client import Client as DysmsapiClient
|
||||||
|
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
|
||||||
|
except ImportError as exc:
|
||||||
|
logger.exception("Aliyun SMS SDK import failed")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="阿里云短信 SDK 未安装,请联系管理员检查部署依赖",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
client_config = open_api_models.Config(
|
||||||
|
access_key_id=config.aliyun_sms_access_key_id,
|
||||||
|
access_key_secret=config.aliyun_sms_access_key_secret,
|
||||||
|
)
|
||||||
|
client_config.endpoint = config.aliyun_sms_endpoint
|
||||||
|
request = dysms_models.SendSmsRequest(
|
||||||
|
phone_numbers=phone,
|
||||||
|
sign_name=config.aliyun_sms_sign_name,
|
||||||
|
template_code=config.aliyun_sms_template_code,
|
||||||
|
template_param=json.dumps({config.aliyun_sms_template_param_key: code}, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
response = DysmsapiClient(client_config).send_sms_with_options(request, util_models.RuntimeOptions())
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Aliyun SMS send failed for %s: %s", _mask_phone(phone), exc.__class__.__name__)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||||
|
detail="短信发送失败,请稍后再试",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
body = getattr(response, "body", None)
|
||||||
|
response_code = getattr(body, "code", None)
|
||||||
|
if response_code not in (None, "OK"):
|
||||||
|
logger.warning("Aliyun SMS rejected for %s: %s", _mask_phone(phone), response_code)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||||
|
detail="短信发送失败,请稍后再试",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_sms_code() -> str:
|
||||||
|
return f"{random.SystemRandom().randint(0, 999999):06d}"
|
||||||
|
|
||||||
|
|
||||||
|
def _config_text(values: dict[str, str], key: str, default: str) -> str:
|
||||||
|
value = values.get(key)
|
||||||
|
return value.strip() if value is not None else default
|
||||||
|
|
||||||
|
|
||||||
|
def _config_bool(values: dict[str, str], key: str, default: bool) -> bool:
|
||||||
|
value = values.get(key)
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def _config_int(values: dict[str, str], key: str, default: int, *, minimum: int, maximum: int) -> int:
|
||||||
|
value = values.get(key)
|
||||||
|
if value is not None:
|
||||||
|
try:
|
||||||
|
return max(minimum, min(maximum, int(float(value.strip()))))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _mask_phone(phone: str) -> str:
|
||||||
|
if len(phone) != 11:
|
||||||
|
return "***"
|
||||||
|
return f"{phone[:3]}****{phone[-4:]}"
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ httpx>=0.28.0
|
|||||||
redis>=5.0.0
|
redis>=5.0.0
|
||||||
openpyxl>=3.1.5
|
openpyxl>=3.1.5
|
||||||
python-multipart>=0.0.20
|
python-multipart>=0.0.20
|
||||||
|
alibabacloud_dysmsapi20170525>=4.1.2
|
||||||
|
|||||||
Reference in New Issue
Block a user