fix: enforce carrier-specific channel group routing
This commit is contained in:
+4
-2
@@ -182,6 +182,7 @@ export type ClientSmsApplication = {
|
||||
tenantId: string;
|
||||
name: string;
|
||||
scene?: string | null;
|
||||
customerUnitPrice?: number | null;
|
||||
status: string;
|
||||
};
|
||||
|
||||
@@ -264,6 +265,7 @@ export type DictionaryItem = Record<string, unknown> & {
|
||||
export type ChannelGroup = DictionaryItem & {
|
||||
code: string;
|
||||
name: string;
|
||||
carrier: 'mobile' | 'unicom' | 'telecom';
|
||||
description?: string | null;
|
||||
retryEnabled?: boolean;
|
||||
retryTimeLimitHours?: number;
|
||||
@@ -461,7 +463,7 @@ export const adminApi = {
|
||||
request<RechargeOrder>('/admin/billing/manual-recharges', { method: 'POST', body: JSON.stringify(body) }),
|
||||
listEnterpriseApplications: (query: { tenantId?: string; keyword?: string } = {}) =>
|
||||
request<EnterpriseApplication[]>(withQuery('/admin/enterprise-applications', query)),
|
||||
createEnterpriseApplication: (body: { tenantId: string; name: string; scene?: string; dailyLimit?: number; maxPhonesPerTask?: number; templateMismatchMode?: string; ipAllowlist?: string[] }) =>
|
||||
createEnterpriseApplication: (body: { tenantId: string; name: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; maxPhonesPerTask?: number; templateMismatchMode?: string; ipAllowlist?: string[] }) =>
|
||||
request<EnterpriseApplication>('/client/applications', { method: 'POST', tenantId: body.tenantId, body: JSON.stringify(body) }),
|
||||
changeApplicationStatus: (id: string, status: string, reason?: string) =>
|
||||
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}/status`, {
|
||||
@@ -526,7 +528,7 @@ export const adminApi = {
|
||||
body: JSON.stringify({ reason }),
|
||||
}),
|
||||
listChannelGroups: () => request<ChannelGroup[]>('/admin/channel-groups'),
|
||||
createChannelGroup: (body: { code: string; name: string; description?: string; status?: string; retryEnabled?: boolean; retryTimeLimitHours?: number }) =>
|
||||
createChannelGroup: (body: { code: string; name: string; carrier: 'mobile' | 'unicom' | 'telecom'; description?: string; status?: string; retryEnabled?: boolean; retryTimeLimitHours?: number }) =>
|
||||
request<ChannelGroup>('/admin/channel-groups', { method: 'POST', body: JSON.stringify(body) }),
|
||||
addChannelGroupItem: (body: Record<string, unknown>) =>
|
||||
request<DictionaryItem>('/admin/channel-groups/items', { method: 'POST', body: JSON.stringify(body) }),
|
||||
|
||||
@@ -149,7 +149,7 @@ export function AdminChannelGroupFormPage() {
|
||||
const navigate = useNavigate();
|
||||
const { groupId } = useParams();
|
||||
const editing = Boolean(groupId && groupId !== 'new');
|
||||
const [groupName, setGroupName] = useState(editing ? '学医三网专用群' : '');
|
||||
const [groupName, setGroupName] = useState(editing ? '学医移动专用组' : '');
|
||||
const [carrier, setCarrier] = useState<Carrier>('mobile');
|
||||
const [retryEnabled, setRetryEnabled] = useState(false);
|
||||
const [provinceRoutes, setProvinceRoutes] = useState(defaultProvinceRoutes);
|
||||
|
||||
@@ -3,12 +3,27 @@ import { Layers3, Plus, Search, UsersRound } from 'lucide-react';
|
||||
import { Breadcrumb, Button, Input, Modal, Pagination } from '@/components/ui';
|
||||
import { adminApi, type ChannelGroup } from '@/api/adminApi';
|
||||
|
||||
type GroupCarrier = 'mobile' | 'unicom' | 'telecom';
|
||||
|
||||
const carrierOptions: Array<{ label: string; value: GroupCarrier }> = [
|
||||
{ label: '移动', value: 'mobile' },
|
||||
{ label: '联通', value: 'unicom' },
|
||||
{ label: '电信', value: 'telecom' },
|
||||
];
|
||||
|
||||
const carrierLabels: Record<GroupCarrier, string> = {
|
||||
mobile: '移动',
|
||||
unicom: '联通',
|
||||
telecom: '电信',
|
||||
};
|
||||
|
||||
export function AdminChannelGroupsPage() {
|
||||
const [groupName, setGroupName] = useState('');
|
||||
const [groups, setGroups] = useState<ChannelGroup[]>([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [name, setName] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
const [carrier, setCarrier] = useState<GroupCarrier>('mobile');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
function loadData() {
|
||||
@@ -27,11 +42,12 @@ export function AdminChannelGroupsPage() {
|
||||
const filteredGroups = useMemo(() => groups.filter((group) => !groupName.trim() || group.name.includes(groupName.trim())), [groupName, groups]);
|
||||
|
||||
function createGroup() {
|
||||
adminApi.createChannelGroup({ code, name, status: 'active' })
|
||||
adminApi.createChannelGroup({ code, name, carrier, status: 'active' })
|
||||
.then(() => {
|
||||
setModalOpen(false);
|
||||
setName('');
|
||||
setCode('');
|
||||
setCarrier('mobile');
|
||||
loadData();
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '通道组创建失败'));
|
||||
@@ -66,6 +82,7 @@ export function AdminChannelGroupsPage() {
|
||||
<Layers3 size={18} />
|
||||
<strong>{group.name}</strong>
|
||||
</div>
|
||||
<span title="运营商">{carrierLabels[group.carrier] ?? group.carrier}</span>
|
||||
<span title="包含通道数"><UsersRound size={16} />{group.items?.length ?? 0}</span>
|
||||
</header>
|
||||
<div className="channel-group-card__body">
|
||||
@@ -95,6 +112,15 @@ export function AdminChannelGroupsPage() {
|
||||
<div className="admin-system-modal-form">
|
||||
<Input label="通道组编码" onChange={(event) => setCode(event.target.value)} value={code} />
|
||||
<Input label="通道组名称" onChange={(event) => setName(event.target.value)} value={name} />
|
||||
<div className="channel-group-radio-row">
|
||||
<span>运营商</span>
|
||||
{carrierOptions.map((item) => (
|
||||
<label key={item.value}>
|
||||
<input checked={carrier === item.value} onChange={() => setCarrier(item.value)} type="radio" />
|
||||
{item.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ export function AdminSmsApplicationFormPage() {
|
||||
const [appName, setAppName] = useState('');
|
||||
const [scene, setScene] = useState('行业通知');
|
||||
const [dailyLimit, setDailyLimit] = useState('100000');
|
||||
const [customerUnitPrice, setCustomerUnitPrice] = useState('0.0300');
|
||||
const [phoneDailyLimit, setPhoneDailyLimit] = useState('10');
|
||||
const [mismatchPolicy, setMismatchPolicy] = useState('manual_review');
|
||||
const [ipAddress, setIpAddress] = useState('');
|
||||
@@ -53,6 +54,7 @@ export function AdminSmsApplicationFormPage() {
|
||||
name: appName,
|
||||
scene,
|
||||
dailyLimit: Number(dailyLimit) || undefined,
|
||||
customerUnitPrice: Math.round(Number(customerUnitPrice || 0) * 100),
|
||||
maxPhonesPerTask: Number(phoneDailyLimit) || undefined,
|
||||
templateMismatchMode: mismatchPolicy,
|
||||
ipAllowlist: ipAddress ? [ipAddress] : [],
|
||||
@@ -71,9 +73,9 @@ export function AdminSmsApplicationFormPage() {
|
||||
.catch((failure: Error) => setError(failure.message || '短信应用保存失败'));
|
||||
}
|
||||
|
||||
const groupOptions = [
|
||||
const groupOptionsByCarrier = (carrier: ChannelGroup['carrier']) => [
|
||||
{ label: '不配置', value: '' },
|
||||
...groups.map((group) => ({ label: group.name, value: group.id })),
|
||||
...groups.filter((group) => group.carrier === carrier).map((group) => ({ label: group.name, value: group.id })),
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -94,6 +96,7 @@ export function AdminSmsApplicationFormPage() {
|
||||
<Input label="应用名称" onChange={(event) => setAppName(event.target.value)} placeholder="请输入应用名称" required value={appName} />
|
||||
<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="每任务最大号码数" onChange={(event) => setPhoneDailyLimit(event.target.value)} placeholder="10" required value={phoneDailyLimit} />
|
||||
<Select
|
||||
label="不符合模板的短信"
|
||||
@@ -107,9 +110,9 @@ export function AdminSmsApplicationFormPage() {
|
||||
value={mismatchPolicy}
|
||||
/>
|
||||
<Input label="IP 白名单" onChange={(event) => setIpAddress(event.target.value)} placeholder="例如 192.168.1.100/32" value={ipAddress} />
|
||||
<Select label="移动通道组" onChange={(event) => setMobileGroupId(event.target.value)} options={groupOptions} value={mobileGroupId} />
|
||||
<Select label="联通通道组" onChange={(event) => setUnicomGroupId(event.target.value)} options={groupOptions} value={unicomGroupId} />
|
||||
<Select label="电信通道组" onChange={(event) => setTelecomGroupId(event.target.value)} options={groupOptions} value={telecomGroupId} />
|
||||
<Select label="移动通道组" onChange={(event) => setMobileGroupId(event.target.value)} options={groupOptionsByCarrier('mobile')} value={mobileGroupId} />
|
||||
<Select label="联通通道组" onChange={(event) => setUnicomGroupId(event.target.value)} options={groupOptionsByCarrier('unicom')} value={unicomGroupId} />
|
||||
<Select label="电信通道组" onChange={(event) => setTelecomGroupId(event.target.value)} options={groupOptionsByCarrier('telecom')} value={telecomGroupId} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user