feat: add application interface controls
This commit is contained in:
+6
-2
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user