feat: add application queue priority routing
This commit is contained in:
+4
-2
@@ -187,6 +187,7 @@ export type ClientSmsApplication = {
|
||||
name: string;
|
||||
scene?: string | null;
|
||||
customerUnitPrice?: number | null;
|
||||
queuePriority?: 'normal' | 'priority' | string | null;
|
||||
status: string;
|
||||
dailyLimit?: number | null;
|
||||
createdAt?: string;
|
||||
@@ -455,6 +456,7 @@ export type EnterpriseApplication = {
|
||||
status: string;
|
||||
dailyLimit?: number | null;
|
||||
customerUnitPrice?: number | null;
|
||||
queuePriority?: 'normal' | 'priority' | string | null;
|
||||
maxPhonesPerTask?: number | null;
|
||||
templateMismatchMode?: string | null;
|
||||
ipAllowlist?: Array<{ id: string; ipCidr: string; remark?: string | null }>;
|
||||
@@ -550,9 +552,9 @@ export const adminApi = {
|
||||
request<EnterpriseApplication[]>(withQuery('/admin/enterprise-applications', query)),
|
||||
getEnterpriseApplication: (id: string) =>
|
||||
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}`),
|
||||
createEnterpriseApplication: (body: { tenantId: string; name: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; maxPhonesPerTask?: number; templateMismatchMode?: string; ipAllowlist?: string[] }) =>
|
||||
createEnterpriseApplication: (body: { tenantId: string; name: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; queuePriority?: 'normal' | 'priority'; maxPhonesPerTask?: number; templateMismatchMode?: string; ipAllowlist?: string[] }) =>
|
||||
request<EnterpriseApplication>('/admin/enterprise-applications', { method: 'POST', body: JSON.stringify(body) }),
|
||||
updateEnterpriseApplication: (id: string, body: { name?: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; maxPhonesPerTask?: number; templateMismatchMode?: string; ipAllowlist?: string[] }) =>
|
||||
updateEnterpriseApplication: (id: string, body: { name?: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; queuePriority?: 'normal' | 'priority'; maxPhonesPerTask?: number; templateMismatchMode?: string; ipAllowlist?: string[] }) =>
|
||||
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||||
changeApplicationStatus: (id: string, status: string, reason?: string) =>
|
||||
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}/status`, {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Copy, Edit3, Plus, Search, Settings2, Trash2 } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Breadcrumb, Button, Input, Modal, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
|
||||
import { adminApi, type ApplicationCmppParams, type CmppConnectionState, type EnterpriseApplication, type TenantOption } from '@/api/adminApi';
|
||||
|
||||
type SmsApp = {
|
||||
@@ -94,15 +94,17 @@ function AddApplicationModal({
|
||||
title={<div className="template-modal-title"><h2>新建企业应用</h2><p>先选择真实企业,再配置短信应用和三网通道组。</p></div>}
|
||||
>
|
||||
<div className="form-grid app-create-modal">
|
||||
<label className="field">
|
||||
<span>所属企业</span>
|
||||
<select disabled={loading} onChange={(event) => onChange(event.target.value)} value={selectedTenantId}>
|
||||
<option value="">{loading ? '企业加载中...' : '请选择真实企业'}</option>
|
||||
{tenants.map((tenant) => (
|
||||
<option key={tenant.id} value={tenant.id}>{tenant.name}({tenant.code})</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<Select
|
||||
disabled={loading || tenants.length === 0}
|
||||
hint={!loading && tenants.length === 0 ? '暂无可选择企业,请先创建真实企业。' : undefined}
|
||||
label="所属企业"
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
options={[
|
||||
{ label: loading ? '企业加载中...' : '请选择真实企业', value: '' },
|
||||
...tenants.map((tenant) => ({ label: `${tenant.name}(${tenant.code})`, value: tenant.id })),
|
||||
]}
|
||||
value={selectedTenantId}
|
||||
/>
|
||||
<div className="app-create-modal__hint">
|
||||
<strong>{selectedTenantId ? tenants.find((tenant) => tenant.id === selectedTenantId)?.name : '请选择要开通短信应用的企业'}</strong>
|
||||
<span>下一步会进入应用参数、客户单价、IP 白名单和运营商通道组配置。</span>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { ArrowLeft, RadioTower } from 'lucide-react';
|
||||
import { ArrowLeft, Info, RadioTower } from 'lucide-react';
|
||||
import { adminApi, type ChannelGroup, type DictionaryItem, type EnterpriseApplication } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Select, Tag } from '@/components/ui';
|
||||
|
||||
type Carrier = 'mobile' | 'unicom' | 'telecom';
|
||||
type QueuePriority = 'normal' | 'priority';
|
||||
|
||||
const carrierMeta: Record<Carrier, { label: string; description: string }> = {
|
||||
mobile: { label: '移动', description: '移动号码只会进入移动通道组' },
|
||||
@@ -20,6 +21,7 @@ export function AdminSmsApplicationFormPage() {
|
||||
const [scene, setScene] = useState('行业通知');
|
||||
const [dailyLimit, setDailyLimit] = useState('100000');
|
||||
const [customerUnitPrice, setCustomerUnitPrice] = useState('0.0300');
|
||||
const [queuePriority, setQueuePriority] = useState<QueuePriority>('normal');
|
||||
const [phoneDailyLimit, setPhoneDailyLimit] = useState('10');
|
||||
const [mismatchPolicy, setMismatchPolicy] = useState('manual_review');
|
||||
const [ipAddress, setIpAddress] = useState('');
|
||||
@@ -72,6 +74,7 @@ export function AdminSmsApplicationFormPage() {
|
||||
setScene(application.scene ?? '');
|
||||
setDailyLimit(application.dailyLimit ? String(application.dailyLimit) : '');
|
||||
setCustomerUnitPrice(((application.customerUnitPrice ?? 0) / 100).toFixed(4));
|
||||
setQueuePriority(application.queuePriority === 'priority' ? 'priority' : 'normal');
|
||||
setPhoneDailyLimit(application.maxPhonesPerTask ? String(application.maxPhonesPerTask) : '');
|
||||
setMismatchPolicy(application.templateMismatchMode ?? 'reject');
|
||||
setIpAddress(application.ipAllowlist?.map((item) => item.ipCidr).join('\n') ?? '');
|
||||
@@ -106,6 +109,7 @@ export function AdminSmsApplicationFormPage() {
|
||||
scene,
|
||||
dailyLimit: Number(dailyLimit) || undefined,
|
||||
customerUnitPrice: Math.round(Number(customerUnitPrice || 0) * 100),
|
||||
queuePriority,
|
||||
maxPhonesPerTask: Number(phoneDailyLimit) || undefined,
|
||||
templateMismatchMode: mismatchPolicy,
|
||||
ipAllowlist: parseIpAllowlist(ipAddress),
|
||||
@@ -163,6 +167,23 @@ export function AdminSmsApplicationFormPage() {
|
||||
<Input label="应用场景" onChange={(event) => setScene(event.target.value)} placeholder="行业通知/营销推广/验证码" value={scene} />
|
||||
<Input label="日发送数量限制" onChange={(event) => setDailyLimit(event.target.value)} placeholder="100000" required value={dailyLimit} />
|
||||
<Input label="客户单价(元/条)" onChange={(event) => setCustomerUnitPrice(event.target.value)} placeholder="0.0300" required value={customerUnitPrice} />
|
||||
<div className="admin-app-form-row admin-app-form-row--wide">
|
||||
<span>发送队列</span>
|
||||
<div className="radio-row">
|
||||
<label>
|
||||
<input checked={queuePriority === 'priority'} onChange={() => setQueuePriority('priority')} type="radio" />
|
||||
优先队列(行业短信)
|
||||
</label>
|
||||
<label>
|
||||
<input checked={queuePriority === 'normal'} onChange={() => setQueuePriority('normal')} type="radio" />
|
||||
普通队列(会员营销)
|
||||
</label>
|
||||
</div>
|
||||
<div className="admin-app-form-tip">
|
||||
<Info size={17} />
|
||||
<span>优先队列会在发送调度中插队处理,但仍必须经过模板、签名、余额、通道组和通道限速校验。</span>
|
||||
</div>
|
||||
</div>
|
||||
<Input label="每任务最大号码数" onChange={(event) => setPhoneDailyLimit(event.target.value)} placeholder="10" required value={phoneDailyLimit} />
|
||||
<Select
|
||||
label="不符合模板的短信"
|
||||
|
||||
Reference in New Issue
Block a user