feat: add application interface controls

This commit is contained in:
hectorzhao
2026-07-09 18:32:26 +08:00
parent 767f0ca9aa
commit ff0756ff9c
14 changed files with 260 additions and 26 deletions
+6 -2
View File
@@ -606,6 +606,8 @@ export type EnterpriseApplication = {
templateMismatchMode?: string | null;
cmppAccount?: string | null;
cmppEnterpriseCode?: string | null;
interfaceEnabled?: boolean | null;
interfaceType?: 'cmpp20' | string | null;
cmppMaxConnections?: number | null;
cmppWindowSize?: number | null;
ipAllowlist?: Array<{ id: string; ipCidr: string; remark?: string | null }>;
@@ -651,6 +653,8 @@ export type ApplicationCmppParams = {
account: string;
passwordCipher: string;
srcId: string;
interfaceEnabled?: boolean;
interfaceType?: string;
maxConnections: number;
heartbeatSeconds: number;
windowSize: number;
@@ -802,9 +806,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; queuePriority?: 'normal' | 'priority'; maxPhonesPerTask?: number; templateMismatchMode?: string; cmppAccount?: string; cmppEnterpriseCode?: string; passwordCipher?: string; cmppMaxConnections?: number; cmppWindowSize?: number; ipAllowlist?: string[] }) =>
createEnterpriseApplication: (body: { tenantId: string; name: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; queuePriority?: 'normal' | 'priority'; maxPhonesPerTask?: number; templateMismatchMode?: string; cmppAccount?: string; cmppEnterpriseCode?: string; passwordCipher?: string; interfaceEnabled?: boolean; interfaceType?: 'cmpp20'; cmppMaxConnections?: number; cmppWindowSize?: number; 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; queuePriority?: 'normal' | 'priority'; maxPhonesPerTask?: number; templateMismatchMode?: string; cmppAccount?: string; cmppEnterpriseCode?: string; passwordCipher?: string; cmppMaxConnections?: number; cmppWindowSize?: number; ipAllowlist?: string[] }) =>
updateEnterpriseApplication: (id: string, body: { name?: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; queuePriority?: 'normal' | 'priority'; maxPhonesPerTask?: number; templateMismatchMode?: string; cmppAccount?: string; cmppEnterpriseCode?: string; passwordCipher?: string; interfaceEnabled?: boolean; interfaceType?: 'cmpp20'; cmppMaxConnections?: number; cmppWindowSize?: number; 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`, {
@@ -22,6 +22,8 @@ type SmsApp = {
type CmppParams = {
host: string;
port: number;
interfaceEnabled: boolean;
interfaceType: string;
enterpriseCode: string;
account: string;
password: string;
@@ -122,10 +124,14 @@ const connectionStateMeta: Record<CmppConnection['state'], { label: string; tone
function formatCmppParams(app: SmsApp, params?: ApplicationCmppParams | null) {
const cmppParams = params ?? app.cmppParams;
const interfaceEnabled = params?.interfaceEnabled ?? app.cmppParams.interfaceEnabled;
const interfaceType = params?.interfaceType ?? app.cmppParams.interfaceType;
return [
`应用名称: ${app.name}`,
`企业名称: ${app.enterprise}`,
`AppID: ${app.appId}`,
`短信接口: ${interfaceEnabled ? '开通' : '关闭'}`,
`接口类型: ${interfaceType === 'cmpp20' ? 'CMPP2.0' : 'HTTP接口'}`,
`CMPP网关地址: ${'gatewayHost' in cmppParams ? cmppParams.gatewayHost : cmppParams.host}`,
`CMPP网关端口: ${'gatewayPort' in cmppParams ? cmppParams.gatewayPort : cmppParams.port}`,
`企业代码: ${cmppParams.enterpriseCode}`,
@@ -146,6 +152,8 @@ function CmppParamsModal({ app, params, onClose }: { app: SmsApp; params?: Appli
const port = params?.gatewayPort ?? app.cmppParams.port;
const password = params?.passwordCipher ?? app.cmppParams.password;
const srcId = params?.srcId ?? app.cmppParams.accessNumber;
const interfaceEnabled = params?.interfaceEnabled ?? app.cmppParams.interfaceEnabled;
const interfaceType = params?.interfaceType ?? app.cmppParams.interfaceType;
async function copyParams() {
await navigator.clipboard.writeText(paramsText);
@@ -168,6 +176,8 @@ function CmppParamsModal({ app, params, onClose }: { app: SmsApp; params?: Appli
>
<div className="cmpp-param-detail">
<div className="cmpp-param-grid">
<div><span></span><strong>{interfaceEnabled ? '开通' : '关闭'}</strong></div>
<div><span></span><strong>{interfaceType === 'cmpp20' ? 'CMPP2.0' : 'HTTP接口'}</strong></div>
<div><span>CMPP网关地址</span><strong>{host}</strong></div>
<div><span>CMPP网关端口</span><strong>{port}</strong></div>
<div><span></span><strong>{params?.enterpriseCode ?? app.cmppParams.enterpriseCode}</strong></div>
@@ -457,8 +467,8 @@ function mapApplication(application: EnterpriseApplication): SmsApp {
sentToday: application.sentToday ?? 0,
deliveryRate: application.deliveryRate ?? 0,
unitPrice: (application.customerUnitPrice ?? 0) / 100,
cmppStatus: application.cmppStatus === 'connected' ? 'connected' : application.cmppStatus === 'inactive' ? 'inactive' : 'disconnected',
cmppParams: { host: '', port: 0, enterpriseCode: application.cmppEnterpriseCode ?? application.tenant?.code ?? application.tenantId, account: application.cmppAccount ?? application.tenantId, password: '', accessNumber: '', maxConnections: 0, heartbeatSeconds: 30, windowSize: 16, protocolVersion: 'CMPP 3.0' },
cmppStatus: application.interfaceEnabled === false ? 'inactive' : application.cmppStatus === 'connected' ? 'connected' : application.cmppStatus === 'inactive' ? 'inactive' : 'disconnected',
cmppParams: { host: '', port: 0, interfaceEnabled: application.interfaceEnabled !== false, interfaceType: application.interfaceType ?? 'cmpp20', enterpriseCode: application.cmppEnterpriseCode ?? application.tenant?.code ?? application.tenantId, account: application.cmppAccount ?? application.tenantId, password: '', accessNumber: '', maxConnections: 0, heartbeatSeconds: 30, windowSize: 16, protocolVersion: 'CMPP2.0' },
cmppConnections: connections,
};
}
+48 -12
View File
@@ -6,6 +6,7 @@ import { Breadcrumb, Button, Input, Select, Tag } from '@/components/ui';
type Carrier = 'mobile' | 'unicom' | 'telecom';
type QueuePriority = 'normal' | 'priority';
type InterfaceType = 'cmpp20';
const carrierMeta: Record<Carrier, { label: string; description: string }> = {
mobile: { label: '移动', description: '移动号码只会进入移动通道组' },
@@ -25,6 +26,8 @@ export function AdminSmsApplicationFormPage() {
const [cmppAccount, setCmppAccount] = useState('');
const [cmppEnterpriseCode, setCmppEnterpriseCode] = useState('');
const [passwordCipher, setPasswordCipher] = useState(() => generateApplicationPassword());
const [interfaceEnabled, setInterfaceEnabled] = useState(true);
const [interfaceType, setInterfaceType] = useState<InterfaceType>('cmpp20');
const [cmppMaxConnections, setCmppMaxConnections] = useState('1');
const [cmppWindowSize, setCmppWindowSize] = useState('16');
const [phoneDailyLimit, setPhoneDailyLimit] = useState('10');
@@ -83,6 +86,8 @@ export function AdminSmsApplicationFormPage() {
setCmppAccount(application.cmppAccount ?? '');
setCmppEnterpriseCode(application.cmppEnterpriseCode ?? application.tenant?.code ?? '');
setPasswordCipher('');
setInterfaceEnabled(application.interfaceEnabled !== false);
setInterfaceType('cmpp20');
setCmppMaxConnections(String(application.cmppMaxConnections ?? 1));
setCmppWindowSize(String(application.cmppWindowSize ?? 16));
setPhoneDailyLimit(application.maxPhonesPerTask ? String(application.maxPhonesPerTask) : '');
@@ -123,6 +128,8 @@ export function AdminSmsApplicationFormPage() {
cmppAccount: cmppAccount.trim() || undefined,
cmppEnterpriseCode: cmppEnterpriseCode.trim() || undefined,
passwordCipher: passwordCipher.trim() || undefined,
interfaceEnabled,
interfaceType,
cmppMaxConnections: Number(cmppMaxConnections) || 1,
cmppWindowSize: Number(cmppWindowSize) || 16,
maxPhonesPerTask: Number(phoneDailyLimit) || undefined,
@@ -182,18 +189,6 @@ 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} />
<Input label="CMPP 6位账号" onChange={(event) => setCmppAccount(event.target.value)} placeholder="留空自动生成" value={cmppAccount} />
<Input label="企业代码" onChange={(event) => setCmppEnterpriseCode(event.target.value)} placeholder="请输入客户侧企业代码" value={cmppEnterpriseCode} />
<Input
hint={isEdit ? '留空则不修改接口密码;填写 16 位字符后覆盖。' : '默认随机生成,可按需修改。'}
label="接口密码"
onChange={(event) => setPasswordCipher(event.target.value)}
placeholder="16 位接口密码"
suffix={<button aria-label="随机生成接口密码" className="icon-button" onClick={() => setPasswordCipher(generateApplicationPassword())} type="button"><RefreshCw size={15} /></button>}
value={passwordCipher}
/>
<Input label="客户最大连接数" onChange={(event) => setCmppMaxConnections(event.target.value)} placeholder="1" required value={cmppMaxConnections} />
<Input label="客户提交窗口" onChange={(event) => setCmppWindowSize(event.target.value)} placeholder="16" required value={cmppWindowSize} />
<div className="admin-app-form-row admin-app-form-row--wide">
<span></span>
<div className="radio-row">
@@ -223,6 +218,47 @@ export function AdminSmsApplicationFormPage() {
required
value={mismatchPolicy}
/>
</div>
</section>
<section className="ui-detail-section">
<div className="ui-detail-section__header">
<h3></h3>
<p> CMPP </p>
</div>
<div className="admin-app-form-grid">
<div className="admin-app-form-row admin-app-form-row--wide">
<span></span>
<button className={interfaceEnabled ? 'admin-switch is-on' : 'admin-switch'} onClick={() => setInterfaceEnabled((current) => !current)} type="button">
<span />
{interfaceEnabled ? '开通' : '关闭'}
</button>
</div>
<div className="admin-app-form-row admin-app-form-row--wide">
<span></span>
<div className="radio-row">
<label>
<input checked={interfaceType === 'cmpp20'} onChange={() => setInterfaceType('cmpp20')} type="radio" />
CMPP2.0
</label>
<label className="is-disabled">
<input disabled type="radio" />
HTTP接口
</label>
</div>
</div>
<Input label="CMPP 6位账号" onChange={(event) => setCmppAccount(event.target.value)} placeholder="留空自动生成" value={cmppAccount} />
<Input label="企业代码" onChange={(event) => setCmppEnterpriseCode(event.target.value)} placeholder="请输入客户侧企业代码" value={cmppEnterpriseCode} />
<Input
hint={isEdit ? '留空则不修改接口密码;填写 16 位字符后覆盖。' : '默认随机生成,可按需修改。'}
label="接口密码"
onChange={(event) => setPasswordCipher(event.target.value)}
placeholder="16 位接口密码"
suffix={<button aria-label="随机生成接口密码" className="icon-button" onClick={() => setPasswordCipher(generateApplicationPassword())} type="button"><RefreshCw size={15} /></button>}
value={passwordCipher}
/>
<Input label="客户最大连接数" onChange={(event) => setCmppMaxConnections(event.target.value)} placeholder="1" required value={cmppMaxConnections} />
<Input label="客户提交窗口" onChange={(event) => setCmppWindowSize(event.target.value)} placeholder="16" required value={cmppWindowSize} />
<Input label="IP 白名单" onChange={(event) => setIpAddress(event.target.value)} placeholder="多个 IP/CIDR 可用逗号、空格或换行分隔" value={ipAddress} />
</div>
</section>
+5
View File
@@ -1259,6 +1259,11 @@ h3 {
gap: var(--space-2);
}
.radio-row label.is-disabled {
color: var(--color-text-muted);
cursor: not-allowed;
}
.radio-row input[type="radio"] {
accent-color: var(--color-selected);
height: 16px;