feat(sms): align provider and verification flow
This commit is contained in:
1 parent
d0a3972ca7
commit
5091da6a60
20 files changed
+489
-994
No files matched your search
@@ -5,6 +5,7 @@ export const usersApiMethods = {
|
||||
|
||||
getAuthSettings() { return this.request('/settings/auth') },
|
||||
saveAuthSettings(data) { return this.request('/settings/auth', { method: 'POST', body: JSON.stringify(data) }) },
|
||||
testSms(phone) { return this.request('/sms/test', { method: 'POST', body: JSON.stringify({ phone }) }) },
|
||||
|
||||
getNotificationSettings() { return this.request('/settings/notifications') },
|
||||
saveNotificationSettings(data) { return this.request('/settings/notifications', { method: 'POST', body: JSON.stringify(data) }) },
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:icon="pageMeta.icon"
|
||||
:schema="pageMeta.groups"
|
||||
:form-data="formData"
|
||||
:dirty="dirty"
|
||||
:dirty="isSmsPage ? isSmsSettingsDirty : dirty"
|
||||
:loading="loading"
|
||||
:saving="saving"
|
||||
:load-error="loadError"
|
||||
@@ -131,6 +131,7 @@ const showTestModal = ref(false)
|
||||
const testLoading = ref(false)
|
||||
const testResult = ref(null)
|
||||
const serverSchema = ref(null)
|
||||
const smsSavedSettings = ref('')
|
||||
const captchaSecretsToClear = ref(new Set())
|
||||
const captchaProviders = [
|
||||
{ key: 'tencent', label: '腾讯云 Captcha', component: TencentConfig },
|
||||
@@ -144,8 +145,17 @@ const captchaProviderTab = ref('tencent')
|
||||
const resolvedDomain = computed(() => normalizeSchemaDomain(props.domain || route.meta?.canonicalDomain || route.meta?.schemaDomain || resolveDomainFromPath(route.path)))
|
||||
const resolvedSection = computed(() => props.section || route.meta?.section || route.meta?.schemaSection || resolveSectionFromPath(route.path))
|
||||
const isEmailPage = computed(() => resolvedDomain.value === 'global' && resolvedSection.value === 'email')
|
||||
const isSmsPage = computed(() => resolvedDomain.value === 'users' && resolvedSection.value === 'sms')
|
||||
const isPermissionsLogin = computed(() => resolvedDomain.value === 'permissions' && resolvedSection.value === 'login')
|
||||
const isPermissionsModeration = computed(() => resolvedDomain.value === 'permissions' && resolvedSection.value === 'moderation')
|
||||
const isSmsSettingsDirty = computed(() => smsSavedSettings.value !== '' && smsSavedSettings.value !== smsSettingsSnapshot(formData.value))
|
||||
|
||||
function smsSettingsSnapshot(data) {
|
||||
const settings = { ...(data || {}) }
|
||||
delete settings.sms_test_phone
|
||||
delete settings.sms_test_send
|
||||
return JSON.stringify(settings)
|
||||
}
|
||||
|
||||
function resolveDomainFromPath(path) {
|
||||
const parts = path.split('/').filter(Boolean)
|
||||
@@ -216,6 +226,10 @@ const {
|
||||
delete payload[key]
|
||||
}
|
||||
})
|
||||
if (isSmsPage.value) {
|
||||
delete payload.sms_test_phone
|
||||
delete payload.sms_test_send
|
||||
}
|
||||
if (clearSecrets.length) payload.__clear_secrets = clearSecrets
|
||||
if (isPermissionsLogin.value && captchaSecretsToClear.value.size) {
|
||||
payload.__clear_secrets = [...(payload.__clear_secrets || []), ...captchaSecretsToClear.value]
|
||||
@@ -226,12 +240,34 @@ const {
|
||||
return response
|
||||
},
|
||||
defaults: defaults.value,
|
||||
onLoaded: (data) => {
|
||||
if (isSmsPage.value) smsSavedSettings.value = smsSettingsSnapshot(data)
|
||||
},
|
||||
onSaved: () => {
|
||||
if (isSmsPage.value) smsSavedSettings.value = smsSettingsSnapshot(formData.value)
|
||||
message.success('设置已保存')
|
||||
},
|
||||
})
|
||||
|
||||
async function handleFieldAction(key) {
|
||||
if (key === 'sms_test_send' && isSmsPage.value) {
|
||||
const phone = String(formData.value?.sms_test_phone || '').trim()
|
||||
if (!phone) {
|
||||
message.warning('请先填写测试手机号')
|
||||
return
|
||||
}
|
||||
if (isSmsSettingsDirty.value) {
|
||||
message.warning('请先保存当前配置,再发送测试短信')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const response = await api.testSms(phone)
|
||||
message.success(response?.message || '测试短信已发送')
|
||||
} catch (error) {
|
||||
message.error(error?.message || '测试短信发送失败')
|
||||
}
|
||||
return
|
||||
}
|
||||
if (key !== 'email_test_send' || !isEmailPage.value) return
|
||||
const to = String(formData.value?.email_test_to || '').trim()
|
||||
if (!to) {
|
||||
|
||||
@@ -2,143 +2,98 @@ export const smsSchema = {
|
||||
key: 'users.sms',
|
||||
title: '短信接口',
|
||||
icon: 'fa fas fa-comment',
|
||||
description: '配置短信验证码和短信通知服务的供应商、模板和限流策略。',
|
||||
description: '设置手机验证码服务。验证码为 6 位数字,有效期 30 分钟。',
|
||||
defaults: {
|
||||
sms_enabled: false,
|
||||
sms_provider: 'aliyun',
|
||||
sms_sdk: 'null',
|
||||
sms_number_limit_ch: true,
|
||||
sms_access_key: '',
|
||||
sms_access_secret: '',
|
||||
sms_sign_name: '',
|
||||
sms_template_code: '',
|
||||
sms_code_length: 6,
|
||||
sms_code_expire: 300,
|
||||
sms_rate_limit: 60,
|
||||
sms_daily_limit: 10,
|
||||
sms_notify_bind: true,
|
||||
sms_notify_login: false,
|
||||
sms_notify_register: true,
|
||||
sms_notify_reset_password: true,
|
||||
sms_ali_api_type: 'sms',
|
||||
sms_ali_keyid: '',
|
||||
sms_ali_keysecret: '',
|
||||
sms_ali_sign_name: '',
|
||||
sms_ali_template_code: '',
|
||||
sms_region_id: 'cn-hangzhou',
|
||||
sms_tencent_app_id: '',
|
||||
sms_tencent_app_key: '',
|
||||
sms_tencent_secret_id: '',
|
||||
sms_tencent_secret_key: '',
|
||||
sms_tencent_region: 'ap-guangzhou',
|
||||
sms_huawei_sender: '',
|
||||
sms_huawei_signature: '',
|
||||
sms_huawei_endpoint: '',
|
||||
sms_custom_url: '',
|
||||
sms_custom_method: 'POST',
|
||||
sms_custom_auth_type: 'header',
|
||||
sms_custom_auth_key: '',
|
||||
sms_custom_auth_value: '',
|
||||
sms_custom_headers: '',
|
||||
sms_custom_body: '',
|
||||
sms_custom_success_rule: '',
|
||||
sms_tencent_sign_name: '',
|
||||
sms_tencent_template_id: '',
|
||||
sms_smsbao_username: '',
|
||||
sms_smsbao_password: '',
|
||||
sms_smsbao_api_key: '',
|
||||
sms_smsbao_template: '',
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'provider',
|
||||
title: '供应商配置',
|
||||
title: '短信接口',
|
||||
icon: 'fa fas fa-plug',
|
||||
fields: [
|
||||
{ key: 'sms_enabled', label: '启用短信服务', type: 'switch', default: false, help: '开启后支持短信验证码和短信通知' },
|
||||
{
|
||||
key: 'sms_provider',
|
||||
label: '短信供应商',
|
||||
key: 'sms_sdk',
|
||||
label: '设置短信接口',
|
||||
type: 'select',
|
||||
default: 'aliyun',
|
||||
default: 'null',
|
||||
options: [
|
||||
{ label: '阿里云短信', value: 'aliyun' },
|
||||
{ label: '不使用', value: 'null' },
|
||||
{ label: '阿里云短信', value: 'ali' },
|
||||
{ label: '腾讯云短信', value: 'tencent' },
|
||||
{ label: '华为云短信', value: 'huawei' },
|
||||
{ label: '自定义接口', value: 'custom' },
|
||||
{ label: '短信宝', value: 'smsbao' },
|
||||
],
|
||||
dependsOn: { key: 'sms_enabled', value: true },
|
||||
},
|
||||
{ key: 'sms_number_limit_ch', label: '仅发国内', type: 'switch', default: true, dependsOn: { key: 'sms_enabled', value: true }, help: '开启后仅支持发送国内短信,关闭后可发送港澳台及国际短信' },
|
||||
{ key: 'sms_access_key', label: 'AccessKey / SecretId / AppKey', type: 'input', default: '', dependsOn: { key: 'sms_enabled', value: true }, help: '阿里云填 AccessKey,腾讯云填 SecretId,华为云填 AppKey' },
|
||||
{ key: 'sms_access_secret', label: 'AccessSecret / SecretKey / AppSecret', type: 'input', inputType: 'password', default: '', dependsOn: { key: 'sms_enabled', value: true } },
|
||||
{
|
||||
key: 'sms_number_limit_ch',
|
||||
label: '限制国内手机号',
|
||||
type: 'switch',
|
||||
default: true,
|
||||
help: '开启后只允许发送到中国大陆手机号。',
|
||||
},
|
||||
{ key: 'sms_test_phone', label: '测试手机号', type: 'input', default: '', ui_only: true, help: '保存接口配置后,发送固定验证码 888888 测试短信。' },
|
||||
{ key: 'sms_test_send', label: '发送测试短信', type: 'button', buttonType: 'primary', buttonText: '发送测试短信', ui_only: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'aliyun',
|
||||
title: '阿里云配置',
|
||||
title: '阿里云',
|
||||
icon: 'fa fas fa-cloud',
|
||||
fields: [
|
||||
{ key: 'sms_ali_api_type', label: '接口类型', type: 'radio', default: 'sms', options: [{ label: '短信服务', value: 'sms' }, { label: '号码认证', value: 'verify' }], dependsOn: { key: 'sms_provider', value: 'aliyun' } },
|
||||
{ key: 'sms_region_id', label: 'RegionId', type: 'input', default: 'cn-hangzhou', dependsOn: { key: 'sms_provider', value: 'aliyun' } },
|
||||
{
|
||||
key: 'sms_ali_api_type',
|
||||
label: '产品',
|
||||
type: 'radio',
|
||||
default: 'sms',
|
||||
options: [
|
||||
{ label: '短信服务', value: 'sms' },
|
||||
{ label: '号码认证:短信认证服务', value: 'verify' },
|
||||
],
|
||||
dependsOn: { key: 'sms_sdk', value: 'ali' },
|
||||
},
|
||||
{ key: 'sms_ali_keyid', label: 'AccessKey Id', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'ali' } },
|
||||
{ key: 'sms_ali_keysecret', label: 'AccessKey Secret', type: 'input', inputType: 'password', secret: true, default: '', dependsOn: { key: 'sms_sdk', value: 'ali' } },
|
||||
{ key: 'sms_ali_sign_name', label: '签名', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'ali' } },
|
||||
{ key: 'sms_ali_template_code', label: '模板 CODE', type: 'input', default: '', dependsOn: { all: [{ key: 'sms_sdk', value: 'ali' }, { key: 'sms_ali_api_type', not: true, value: 'verify' }] }, help: '短信服务需要填写。号码认证产品固定使用平台模板。' },
|
||||
{ key: 'sms_region_id', label: 'RegionId', type: 'input', default: 'cn-hangzhou', dependsOn: { key: 'sms_sdk', value: 'ali' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'tencent',
|
||||
title: '腾讯云配置',
|
||||
title: '腾讯云',
|
||||
icon: 'fa fas fa-cloud',
|
||||
fields: [
|
||||
{ key: 'sms_tencent_app_id', label: 'SdkAppId', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'tencent' }, help: '腾讯云短信应用 ID' },
|
||||
{ key: 'sms_tencent_app_key', label: 'AppKey', type: 'input', inputType: 'password', default: '', dependsOn: { key: 'sms_provider', value: 'tencent' } },
|
||||
{ key: 'sms_tencent_secret_id', label: 'SecretId', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'tencent' } },
|
||||
{ key: 'sms_tencent_secret_key', label: 'SecretKey', type: 'input', inputType: 'password', default: '', dependsOn: { key: 'sms_provider', value: 'tencent' } },
|
||||
{ key: 'sms_tencent_region', label: '地域', type: 'input', default: 'ap-guangzhou', dependsOn: { key: 'sms_provider', value: 'tencent' } },
|
||||
{ key: 'sms_tencent_app_id', label: 'SDK AppID', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'tencent' } },
|
||||
{ key: 'sms_tencent_app_key', label: 'App Key', type: 'input', inputType: 'password', secret: true, default: '', dependsOn: { key: 'sms_sdk', value: 'tencent' } },
|
||||
{ key: 'sms_tencent_sign_name', label: '签名', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'tencent' } },
|
||||
{ key: 'sms_tencent_template_id', label: '模板 ID', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'tencent' }, help: '模板包含验证码和有效分钟数两个参数。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'huawei',
|
||||
title: '华为云配置',
|
||||
key: 'smsbao',
|
||||
title: '短信宝',
|
||||
icon: 'fa fas fa-cloud',
|
||||
fields: [
|
||||
{ key: 'sms_huawei_sender', label: '通道号 (sender)', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'huawei' } },
|
||||
{ key: 'sms_huawei_signature', label: '短信签名', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'huawei' }, help: '华为云短信签名内容' },
|
||||
{ key: 'sms_huawei_endpoint', label: 'API Endpoint', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'huawei' }, help: '留空使用默认 endpoint' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'custom',
|
||||
title: '自定义接口配置',
|
||||
icon: 'fa fas fa-plug',
|
||||
fields: [
|
||||
{ key: 'sms_custom_url', label: '请求 URL', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: '支持 {phone} {code} {time} 变量' },
|
||||
{ key: 'sms_custom_method', label: '请求方法', type: 'select', default: 'POST', options: [{ label: 'POST', value: 'POST' }, { label: 'GET', value: 'GET' }, { label: 'PUT', value: 'PUT' }], dependsOn: { key: 'sms_provider', value: 'custom' } },
|
||||
{ key: 'sms_custom_auth_type', label: '鉴权方式', type: 'select', default: 'header', options: [{ label: 'Header', value: 'header' }], dependsOn: { key: 'sms_provider', value: 'custom' } },
|
||||
{ key: 'sms_custom_auth_key', label: '鉴权 Header 名', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'custom' } },
|
||||
{ key: 'sms_custom_auth_value', label: '鉴权 Header 值', type: 'input', inputType: 'password', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: '支持 {phone} {code} {time} 变量' },
|
||||
{ key: 'sms_custom_headers', label: '自定义 Headers', type: 'textarea', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: '每行一个,格式:Key: Value' },
|
||||
{ key: 'sms_custom_body', label: '请求 Body', type: 'textarea', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: 'JSON 格式,支持 {phone} {code} {time} 变量' },
|
||||
{ key: 'sms_custom_success_rule', label: '成功判定规则', type: 'input', default: '', dependsOn: { key: 'sms_provider', value: 'custom' }, help: '格式:status:200 或 json:code=0 或 contains:success' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'template',
|
||||
title: '短信模板',
|
||||
icon: 'fa fas fa-comment',
|
||||
fields: [
|
||||
{ key: 'sms_sign_name', label: '短信签名', type: 'input', default: '', dependsOn: { key: 'sms_enabled', value: true }, help: '在短信服务商申请的短信签名(所有供应商共用此字段)' },
|
||||
{ key: 'sms_template_code', label: '验证码模板 ID', type: 'input', default: '', dependsOn: { key: 'sms_enabled', value: true }, help: '所有供应商共用此模板 ID' },
|
||||
{ key: 'sms_code_length', label: '验证码长度', type: 'number', default: 6, min: 4, max: 8, dependsOn: { key: 'sms_enabled', value: true } },
|
||||
{ key: 'sms_code_expire', label: '验证码有效期 (秒)', type: 'number', default: 300, min: 60, max: 600, dependsOn: { key: 'sms_enabled', value: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'rate_limit',
|
||||
title: '限流策略',
|
||||
icon: 'fa fas fa-shield-alt',
|
||||
fields: [
|
||||
{ key: 'sms_rate_limit', label: '发送间隔 (秒)', type: 'number', default: 60, min: 10, max: 300, dependsOn: { key: 'sms_enabled', value: true }, help: '同一手机号两次发送的最小间隔' },
|
||||
{ key: 'sms_daily_limit', label: '每日发送上限', type: 'number', default: 10, min: 1, max: 100, dependsOn: { key: 'sms_enabled', value: true }, help: '同一手机号每天最多发送条数' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'triggers',
|
||||
title: '通知场景',
|
||||
icon: 'fa fas fa-bell',
|
||||
fields: [
|
||||
{ key: 'sms_notify_bind', label: '绑定手机时验证', type: 'switch', default: true, dependsOn: { key: 'sms_enabled', value: true } },
|
||||
{ key: 'sms_notify_login', label: '短信登录', type: 'switch', default: false, dependsOn: { key: 'sms_enabled', value: true } },
|
||||
{ key: 'sms_notify_register', label: '注册验证', type: 'switch', default: true, dependsOn: { key: 'sms_enabled', value: true } },
|
||||
{ key: 'sms_notify_reset_password', label: '找回密码', type: 'switch', default: true, dependsOn: { key: 'sms_enabled', value: true } },
|
||||
{ key: 'sms_smsbao_username', label: '用户名', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'smsbao' } },
|
||||
{ key: 'sms_smsbao_password', label: '密码', type: 'input', inputType: 'password', secret: true, default: '', dependsOn: { key: 'sms_sdk', value: 'smsbao' } },
|
||||
{ key: 'sms_smsbao_api_key', label: 'ApiKey', type: 'input', inputType: 'password', secret: true, default: '', dependsOn: { key: 'sms_sdk', value: 'smsbao' }, help: '可选;填写后以 ApiKey 认证,可与密码任选其一。' },
|
||||
{ key: 'sms_smsbao_template', label: '模板内容', type: 'input', default: '', dependsOn: { key: 'sms_sdk', value: 'smsbao' }, help: '必须包含 {code},有效时间变量为 {time}。' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user