feat: add CRM lead assignment strategies

This commit is contained in:
Codex
2026-06-29 20:51:24 +08:00
parent f6710ace8b
commit ec23cd3b9b
10 changed files with 375 additions and 20 deletions

View File

@@ -62,7 +62,17 @@ export default function TenantMarketingPage() {
const [commissionOrders, setCommissionOrders] = useState<CommissionOrderItem[]>([]);
const [settlements, setSettlements] = useState<CommissionSettlementItem[]>([]);
const [members, setMembers] = useState<TenantMemberItem[]>([]);
const [crmForm, setCrmForm] = useState({ enabled: false, url: '', secretRef: '', formName: '刷题题库', examType: '专升本', timeoutSec: '10', delaySec: '60' });
const [crmForm, setCrmForm] = useState({
enabled: false,
url: '',
secretRef: '',
formName: '刷题题库',
examType: '专升本',
timeoutSec: '10',
delaySec: '60',
assignmentMode: 'none' as 'none' | 'direct' | 'round_robin' | 'referrer',
assignmentPoolText: '',
});
const [commissionForm, setCommissionForm] = useState({ defaultRatePercent: '20', minSettlementYuan: '0', settlementCycle: 'monthly' });
const [period, setPeriod] = useState({ startDate: monthStart(), endDate: today(), referrerUserId: '' });
const [memberRate, setMemberRate] = useState({ userId: '', ratePercent: '' });
@@ -112,6 +122,8 @@ export default function TenantMarketingPage() {
examType: nextCrm.examType || '专升本',
timeoutSec: String(nextCrm.timeoutSec || 10),
delaySec: String(nextCrm.delaySec || 60),
assignmentMode: nextCrm.assignmentMode || 'none',
assignmentPoolText: (nextCrm.assignmentPool || []).join(','),
});
}
if (nextSettings) {
@@ -142,6 +154,8 @@ export default function TenantMarketingPage() {
examType: crmForm.examType.trim() || '专升本',
timeoutSec: Math.max(1, Math.trunc(Number(crmForm.timeoutSec || 10))),
delaySec: Math.max(0, Math.trunc(Number(crmForm.delaySec || 60))),
assignmentMode: crmForm.assignmentMode,
assignmentPool: crmForm.assignmentPoolText.split(',').map(item => item.trim()).filter(Boolean),
});
setCrmConfig(result.item || null);
Taro.showToast({ title: 'CRM 已保存', icon: 'success' });
@@ -304,6 +318,18 @@ export default function TenantMarketingPage() {
<Input className='admin-input' placeholder='考试类型' value={crmForm.examType} onInput={event => setCrmForm(prev => ({ ...prev, examType: String(event.detail.value || '') }))} />
<Input className='admin-input' type='number' placeholder='超时秒数' value={crmForm.timeoutSec} onInput={event => setCrmForm(prev => ({ ...prev, timeoutSec: String(event.detail.value || '') }))} />
<Input className='admin-input' type='number' placeholder='延迟推送秒数' value={crmForm.delaySec} onInput={event => setCrmForm(prev => ({ ...prev, delaySec: String(event.detail.value || '') }))} />
<View className='admin-actions compact'>
{(['none', 'referrer', 'direct', 'round_robin'] as const).map(mode => (
<Button
key={mode}
className={`admin-button ${crmForm.assignmentMode === mode ? 'active' : ''}`}
onClick={() => setCrmForm(prev => ({ ...prev, assignmentMode: mode }))}
>
{mode === 'none' ? '不分配' : mode === 'referrer' ? '归属人跟进' : mode === 'direct' ? '固定负责人' : '轮询分配'}
</Button>
))}
</View>
<Input className='admin-input' placeholder='负责人用户ID多个用英文逗号分隔' value={crmForm.assignmentPoolText} onInput={event => setCrmForm(prev => ({ ...prev, assignmentPoolText: String(event.detail.value || '') }))} />
</View>
<View className='admin-actions compact'>
<Button className='admin-button primary' loading={busy === 'crm'} onClick={saveCrmConfig}> CRM</Button>
@@ -312,6 +338,15 @@ export default function TenantMarketingPage() {
<View className='admin-row'>
<Text className='admin-row-main'>{crmConfig?.enabled ? 'CRM 已启用' : 'CRM 未启用'}</Text>
<Text className='admin-row-meta'>{crmConfig?.url || '未配置 URL'} · {crmConfig?.secretRef || '未配置 secretRef'}</Text>
<Text className='admin-row-meta'> {crmConfig?.assignmentMode || 'none'} · {crmConfig?.assignmentPool?.length || 0} · {crmConfig?.assignmentCursor || 0}</Text>
</View>
<View className='admin-list compact'>
{salesMembers.slice(0, 8).map(item => (
<View className='admin-row' key={item.userId}>
<Text className='admin-row-main'>{item.name || item.username || item.userId}</Text>
<Text className='admin-row-meta'>{item.role} · {item.userId}</Text>
</View>
))}
</View>
</View>

View File

@@ -421,6 +421,10 @@ export interface CrmConfigItem {
examType?: string | null;
timeoutSec?: number;
delaySec?: number;
assignmentMode?: 'none' | 'direct' | 'round_robin' | 'referrer';
assignmentPool?: string[];
assignmentCursor?: number;
assignmentConfig?: Record<string, unknown>;
updatedAt?: string;
}
@@ -1017,6 +1021,9 @@ export async function upsertCrmConfig(input: {
examType?: string | null;
timeoutSec?: number;
delaySec?: number;
assignmentMode?: 'none' | 'direct' | 'round_robin' | 'referrer';
assignmentPool?: string[];
assignmentConfig?: Record<string, unknown>;
}) {
return apiRequest<{ item?: CrmConfigItem }>('/api/crm/config', {
method: 'PUT',