feat(sms): 新增节点短信提醒(审核未通过/被租用需辅助/上架未成交)

This commit is contained in:
yml2213
2026-10-14 14:57:52 +08:00
parent efa0aa4ffd
commit 93e102b7c0
38 changed files with 3228 additions and 70 deletions
@@ -33,6 +33,7 @@ import {
} from '@/features/admin/api/adminDistribution'
import { formatDateTime } from '@/shared/utils/time'
import { readError } from '@/shared/utils/error'
import { smsNodeMetas } from '@/features/admin/api/smsNodeTemplates'
import {
distributionAdminBaseUrl,
distributionAdminLoginUrl,
@@ -143,6 +144,7 @@ const smsForm = reactive({
masked_secret: '',
has_secret: false,
endpoint: '',
business_templates: {} as Record<string, string>,
})
function handleOpenSettlement(row: DistributionSite) {
@@ -208,6 +210,10 @@ async function handleOpenSMS(row: DistributionSite) {
smsForm.masked_secret = cfg.masked_secret || ''
smsForm.has_secret = cfg.has_secret
smsForm.endpoint = cfg.endpoint || ''
// 逐节点初始化,未配置的节点留空表示回退主站模板。
smsForm.business_templates = Object.fromEntries(
smsNodeMetas.map(meta => [meta.code, cfg.business_templates?.[meta.code] || ''])
)
} catch (err) {
ElMessage.error(readError(err, '读取分站短信配置失败'))
} finally {
@@ -215,6 +221,15 @@ async function handleOpenSMS(row: DistributionSite) {
}
}
function trimmedBusinessTemplates() {
const out: Record<string, string> = {}
for (const meta of smsNodeMetas) {
const code = (smsForm.business_templates[meta.code] || '').trim()
if (code) out[meta.code] = code
}
return out
}
async function handleSMSSubmit() {
if (smsForm.enabled) {
if (!smsForm.sign_name.trim()) {
@@ -244,6 +259,7 @@ async function handleSMSSubmit() {
access_key_id: string
access_key_secret?: string
endpoint?: string
business_templates: Record<string, string>
} = {
enabled: smsForm.enabled,
provider: smsForm.provider || 'aliyun',
@@ -251,6 +267,7 @@ async function handleSMSSubmit() {
login_template_code: smsForm.login_template_code.trim(),
access_key_id: smsForm.access_key_id.trim(),
endpoint: smsForm.endpoint.trim(),
business_templates: trimmedBusinessTemplates(),
}
if (smsSecretInput.value.trim()) {
payload.access_key_secret = smsSecretInput.value.trim()
@@ -1082,6 +1099,14 @@ onMounted(() => {
<el-form-item label="API Endpoint">
<el-input v-model="smsForm.endpoint" placeholder="默认 dysmsapi.aliyuncs.com" />
</el-form-item>
<el-divider content-position="left">节点短信模板(可选)</el-divider>
<el-form-item v-for="meta in smsNodeMetas" :key="meta.code" :label="meta.name">
<el-input
v-model="smsForm.business_templates[meta.code]"
:placeholder="`留空则用主站模板,例如:${meta.templateCode}`"
/>
<div class="sms-node-hint">{{ meta.description }}</div>
</el-form-item>
<el-form-item label="连通性测试">
<div style="display: flex; gap: 8px; width: 100%">
<el-input v-model="smsTestPhone" placeholder="输入接收测试短信的手机号" />
@@ -1344,4 +1369,12 @@ onMounted(() => {
color: #6b7280;
font-size: 13px;
}
.sms-node-hint {
width: 100%;
margin-top: 4px;
color: #9ca3af;
font-size: 12px;
line-height: 1.5;
}
</style>
@@ -19,6 +19,10 @@ import {
import type { OrderAgreements } from '@/features/orders/api/orders'
import { fetchSystemConfigs, type SystemConfig } from '@/features/admin/api/systemConfigs'
import { parseRenterGrowthConfig } from '@/features/admin/api/renterGrowth'
import {
parseSmsBusinessTemplatesConfig,
smsNodeMetas,
} from '@/features/admin/api/smsNodeTemplates'
import { formatDateTime } from '@/shared/utils/time'
import { safeParseJSON } from '@/shared/utils/json'
import { formatSystemConfigSelectValue } from '@/shared/utils/systemConfigOptions'
@@ -40,6 +44,7 @@ import OrderAgreementsDialog from '../components/OrderAgreementsDialog.vue'
import PostRentalNoticeDialog from '../components/PostRentalNoticeDialog.vue'
import GeneralConfigDialog from '../components/GeneralConfigDialog.vue'
import RenterGrowthConfigDialog from '../components/RenterGrowthConfigDialog.vue'
import SmsNodeTemplatesDialog from '../components/SmsNodeTemplatesDialog.vue'
import AutoWelcomeConfig from '../components/AutoWelcomeConfig.vue'
import {
fetchAutoWelcomeMessage,
@@ -86,6 +91,7 @@ const agreementsVisible = ref(false)
const postRentalNoticeVisible = ref(false)
const generalVisible = ref(false)
const renterGrowthVisible = ref(false)
const smsNodeTemplatesVisible = ref(false)
const publishConfig = computed(
() => configs.value.find(item => item.key === 'listing.publish_options') || null
@@ -99,6 +105,9 @@ const listingPublishCooldownConfig = computed(
const renterGrowthConfig = computed(
() => configs.value.find(item => item.key === 'renter.growth_level_rules') || null
)
const smsNodeTemplatesConfig = computed(
() => configs.value.find(item => item.key === 'sms.business_templates') || null
)
const homeAnnouncementsConfig = computed(
() => configs.value.find(item => item.key === 'mobile.home_announcements') || null
)
@@ -167,7 +176,8 @@ const regularConfigs = computed(() =>
item.key !== 'integration.paddle_ocr_token' &&
item.key !== 'integration.paddle_ocr_job_url' &&
item.key !== 'integration.paddle_ocr_model' &&
item.key !== 'renter.growth_level_rules'
item.key !== 'renter.growth_level_rules' &&
item.key !== 'sms.business_templates'
)
)
@@ -227,6 +237,24 @@ const renterGrowthStats = computed(() => {
}
})
const smsNodeStats = computed(() => {
const config = parseSmsBusinessTemplatesConfig(smsNodeTemplatesConfig.value?.value || '')
const configured = smsNodeMetas.filter(
meta => config.nodes[meta.code]?.template_code && config.nodes[meta.code]?.enabled
)
return {
enabled: config.enabled,
configuredCount: configured.length,
totalCount: smsNodeMetas.length,
nodes: smsNodeMetas,
statusOf: (code: string) => {
const node = config.nodes[code]
if (!node?.template_code) return '未配置'
return node.enabled ? '已配置' : '已停用'
},
}
})
const agreementStats = computed(() => {
const agreements = parseOrderAgreements(orderAgreementsConfig.value?.value || '')
return {
@@ -295,6 +323,8 @@ function openEdit(row: SystemConfig) {
postRentalNoticeVisible.value = true
} else if (row.key === 'renter.growth_level_rules') {
renterGrowthVisible.value = true
} else if (row.key === 'sms.business_templates') {
smsNodeTemplatesVisible.value = true
} else if (row.key === 'chat.renter_retention_days_after_order_end') {
generalVisible.value = true
} else {
@@ -592,6 +622,36 @@ function formatConfigValue(row: SystemConfig) {
</div>
</section>
<section v-if="smsNodeTemplatesConfig" class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">Node SMS</p>
<h2>节点短信</h2>
<span
>审核未通过、被租用需辅助登录、上架多日未成交等节点给号主发短信;只填阿里云已报备的模板
CODE,文案由平台模板决定。分站可在「分销站点 → 短信配置」里配置自己的模板。</span
>
</div>
<el-button type="primary" @click="openEdit(smsNodeTemplatesConfig)">
编辑节点短信
</el-button>
</div>
<div class="publish-stat-grid home-stat-grid">
<div class="publish-stat">
<strong>{{ smsNodeStats.enabled ? '已启用' : '已停用' }}</strong>
<span>总开关</span>
</div>
<div class="publish-stat">
<strong>{{ smsNodeStats.configuredCount }} / {{ smsNodeStats.totalCount }}</strong>
<span>已配置节点</span>
</div>
<div v-for="meta in smsNodeStats.nodes" :key="meta.code" class="publish-stat">
<strong>{{ smsNodeStats.statusOf(meta.code) }}</strong>
<span>{{ meta.name }}</span>
</div>
</div>
</section>
<section
v-if="
homeAnnouncementsConfig || homeBannersConfig || awRecycleConfig || cooperationFeedbackConfig
@@ -956,6 +1016,13 @@ function formatConfigValue(row: SystemConfig) {
@saved="loadConfigs"
/>
<SmsNodeTemplatesDialog
v-if="smsNodeTemplatesConfig"
v-model="smsNodeTemplatesVisible"
:config="smsNodeTemplatesConfig"
@saved="loadConfigs"
/>
<GeneralConfigDialog
v-if="currentEditingConfig"
v-model="generalVisible"