feat: 完善商机管理模块,移除线索自动创建商机逻辑

- 商机管理页面:
  - 实现crmStats真实数据计算(商机总数、金额、本月赢单、转化率)
  - 添加环比变化率计算功能
  - 修复金额字段类型转换问题(字符串转数字)
  - 完善Pipeline View阶段过滤和列显示
  - 添加商机搜索、详情查看、编辑和删除功能

- 后端改造:
  - 移除线索转化自动创建商机的逻辑(业务流程修正)
  - 新增经销商选择器API(/crm/dealer/portal/select)
  - 实现经销商列表查询接口

- 新增50条商机测试数据SQL脚本

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
大壮
2026-05-21 09:47:40 +00:00
parent 3f643ef31f
commit d42ad5e1e1
8 changed files with 888 additions and 30 deletions

View File

@@ -5,6 +5,7 @@
*/
import type {
CrmDealerVo,
CrmLeadBo,
CrmLeadFollowBo,
CrmLeadFollowVo,
@@ -21,6 +22,7 @@ import type {
// 导出类型供外部使用
export type {
CrmDealerVo,
CrmLeadBo,
CrmLeadFollowBo,
CrmLeadFollowVo,
@@ -139,3 +141,17 @@ export function updateOpportunity(data: CrmOpportunityBo): Promise<R<void>> {
export function deleteOpportunity(opportunityIds: string): Promise<R<void>> {
return request.delete(`/crm/opportunity/${opportunityIds}`).json();
}
/**
* ========================================
* CRM 经销商管理模块 API 调用
* ========================================
*/
/**
* 获取经销商选择器列表
* 用于商机创建、商机分配等场景
*/
export function getDealerSelectList(keyword?: string): Promise<R<CrmDealerVo[]>> {
return request.get('/crm/dealer/portal/select', { keyword }).json();
}

View File

@@ -217,4 +217,47 @@ export interface TableDataInfo<T> {
msg: string;
rows: T[];
total: number;
}
/**
* ========================================
* CRM 经销商管理模块类型定义
* ========================================
*/
/**
* 经销商视图对象(响应)
*/
export interface CrmDealerVo {
dealerId: number;
tenantId: string;
customerCode?: string; // ERP客户编码
dealerName: string; // 经销商名称
dealerCode: string; // 经销商编码
contactName?: string; // 联系人
mobile?: string; // 手机
province?: string; // 省
city?: string; // 市
level?: string; // 等级A/B/C
levelName?: string; // 等级名称(翻译)
lifecycle?: string; // 生命周期
lifecycleName?: string; // 生命周期名称(翻译)
signedAt?: string; // 签约时间
storeCount?: number; // 门店数
teamSize?: number; // 团队规模
totalOrderAmount?: number; // 累计订单金额
totalPaymentAmount?: number; // 累计回款金额
activityScore?: number; // 活跃评分
riskScore?: number; // 风险评分
ownerUserId?: number; // 负责人
ownerUserName?: string; // 负责人姓名(翻译)
sourceLeadId?: number; // 来源线索ID
status?: string; // 状态
statusName?: string; // 状态名称(翻译)
createBy: number;
createByName?: string;
createTime: string;
updateBy?: number;
updateByName?: string;
updateTime?: string;
}

View File

@@ -1,18 +1,125 @@
<script setup lang="ts">
import type { CrmOpportunityVo } from '@/api/crm';
import { ElMessage } from 'element-plus';
import type { CrmDealerVo, CrmOpportunityBo, CrmOpportunityVo } from '@/api/crm';
import type { UserInfo } from '@/api/user';
import { ElMessage, ElMessageBox } from 'element-plus';
import { computed, onMounted, ref } from 'vue';
import { getOpportunityList } from '@/api/crm';
import { createOpportunity, deleteOpportunity, getDealerSelectList, getOpportunityList, updateOpportunity } from '@/api/crm';
import { getUserSelectList } from '@/api/user';
// Stats
const opportunityTotal = ref(0);
const crmStats = computed(() => [
{ label: '商机总数', value: opportunityTotal.value.toLocaleString(), icon: 'TrendCharts' as const, change: '+5.2%', up: true, bg: '#eef5ff', color: '#1d5af3' },
{ label: '商机金额', value: '¥4,680万', icon: 'Money' as const, change: '+12.8%', up: true, bg: '#f0fdf4', color: '#16a34a' },
{ label: '本月赢单', value: '23', icon: 'Trophy' as const, change: '+15%', up: true, bg: '#fef3c7', color: '#d97706' },
{ label: '转化率', value: '32%', icon: 'DataLine' as const, change: '-2.1%', up: false, bg: '#fef2f2', color: '#dc2626' },
]);
// Stats - 使用真实数据计算
const crmStats = computed(() => {
// 商机总数
const total = opportunityList.value.length;
// 商机金额总和amount可能返回字符串需转换为数字
const totalAmount = opportunityList.value.reduce((sum, o) => {
const amount = typeof o.amount === 'string' ? parseFloat(o.amount) : (o.amount || 0);
return sum + amount;
}, 0);
// 本月赢单数量closing阶段
const now = new Date();
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
const lastMonth = currentMonth === 0 ? 11 : currentMonth - 1;
const lastMonthYear = currentMonth === 0 ? currentYear - 1 : currentYear;
// 本月赢单
const monthlyWins = opportunityList.value.filter((o) => {
if (o.stage !== 'closing') return false;
const createTime = new Date(o.createTime);
return createTime.getMonth() === currentMonth && createTime.getFullYear() === currentYear;
}).length;
// 上月赢单(用于计算环比)
const lastMonthWins = opportunityList.value.filter((o) => {
if (o.stage !== 'closing') return false;
const createTime = new Date(o.createTime);
return createTime.getMonth() === lastMonth && createTime.getFullYear() === lastMonthYear;
}).length;
// 本月新增商机
const monthlyNewOpps = opportunityList.value.filter((o) => {
const createTime = new Date(o.createTime);
return createTime.getMonth() === currentMonth && createTime.getFullYear() === currentYear;
}).length;
// 上月新增商机(用于计算环比)
const lastMonthNewOpps = opportunityList.value.filter((o) => {
const createTime = new Date(o.createTime);
return createTime.getMonth() === lastMonth && createTime.getFullYear() === lastMonthYear;
}).length;
// 本月赢单金额
const monthlyWinAmount = opportunityList.value.filter((o) => {
if (o.stage !== 'closing') return false;
const createTime = new Date(o.createTime);
return createTime.getMonth() === currentMonth && createTime.getFullYear() === currentYear;
}).reduce((sum, o) => {
const amount = typeof o.amount === 'string' ? parseFloat(o.amount) : (o.amount || 0);
return sum + amount;
}, 0);
// 上月赢单金额
const lastMonthWinAmount = opportunityList.value.filter((o) => {
if (o.stage !== 'closing') return false;
const createTime = new Date(o.createTime);
return createTime.getMonth() === lastMonth && createTime.getFullYear() === lastMonthYear;
}).reduce((sum, o) => {
const amount = typeof o.amount === 'string' ? parseFloat(o.amount) : (o.amount || 0);
return sum + amount;
}, 0);
// 转化率closing阶段占比
const winCount = opportunityList.value.filter(o => o.stage === 'closing').length;
const conversionRate = total > 0 ? Math.round((winCount / total) * 100) : 0;
// 计算环比变化率
function calcChangeRate(current: number, previous: number): { rate: string, up: boolean } {
if (previous === 0) {
// 上月无数据时,显示新增状态
return { rate: current > 0 ? '+新增' : '0%', up: current > 0 };
}
const change = ((current - previous) / previous) * 100;
const sign = change >= 0 ? '+' : '';
return { rate: `${sign}${change.toFixed(1)}%`, up: change >= 0 };
}
// 商机总数变化(本月新增 vs 上月新增)
const totalChange = calcChangeRate(monthlyNewOpps, lastMonthNewOpps);
// 商机金额变化(本月赢单金额 vs 上月赢单金额)
const amountChange = calcChangeRate(monthlyWinAmount, lastMonthWinAmount);
// 本月赢单变化(本月赢单 vs 上月赢单)
const winsChange = calcChangeRate(monthlyWins, lastMonthWins);
// 转化率变化基于closing阶段本月vs上月占比
const currentConversionBase = monthlyNewOpps > 0 ? Math.round((monthlyWins / monthlyNewOpps) * 100) : 0;
const lastConversionBase = lastMonthNewOpps > 0 ? Math.round((lastMonthWins / lastMonthNewOpps) * 100) : 0;
const conversionChange = calcChangeRate(currentConversionBase, lastConversionBase);
return [
{ label: '商机总数', value: total.toString(), icon: 'TrendCharts' as const, change: totalChange.rate, up: totalChange.up, bg: '#eef5ff', color: '#1d5af3' },
{ label: '商机金额', value: formatAmount(totalAmount), icon: 'Money' as const, change: amountChange.rate, up: amountChange.up, bg: '#f0fdf4', color: '#16a34a' },
{ label: '本月赢单', value: monthlyWins.toString(), icon: 'Trophy' as const, change: winsChange.rate, up: winsChange.up, bg: '#fef3c7', color: '#d97706' },
{ label: '转化率', value: `${conversionRate}%`, icon: 'DataLine' as const, change: conversionChange.rate, up: conversionChange.up, bg: '#fef2f2', color: '#dc2626' },
];
});
// 格式化金额显示
function formatAmount(amount: number): string {
if (amount >= 100000000) {
return `¥${(amount / 100000000).toFixed(2)}亿`;
}
if (amount >= 10000) {
return `¥${(amount / 10000).toFixed(2)}`;
}
return `¥${amount.toLocaleString()}`;
}
// Pipeline
const activeStage = ref('all');
@@ -29,6 +136,9 @@ const pipeline = [
const opportunityList = ref<CrmOpportunityVo[]>([]);
const opportunityLoading = ref(false);
// 搜索关键词
const searchKeyword = ref('');
// 加载商机列表
async function loadOpportunities() {
opportunityLoading.value = true;
@@ -56,9 +166,238 @@ function updatePipelineCounts() {
}
function getCardsForStage(stage: string) {
if (stage === 'all')
return opportunityList.value;
return opportunityList.value.filter((o: CrmOpportunityVo) => o.stage === stage);
// 先按阶段过滤
let cards = stage === 'all'
? opportunityList.value
: opportunityList.value.filter((o: CrmOpportunityVo) => o.stage === stage);
// 再按搜索关键词过滤
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase();
cards = cards.filter((o: CrmOpportunityVo) =>
o.opportunityName?.toLowerCase().includes(keyword)
|| o.dealerName?.toLowerCase().includes(keyword)
|| o.ownerUserName?.toLowerCase().includes(keyword)
|| o.productName?.toLowerCase().includes(keyword),
);
}
return cards;
}
// 根据activeStage过滤显示的Pipeline列
const filteredPipeline = computed(() => {
// 实际阶段列(排除"全部"虚拟列)
const actualStages = pipeline.filter(s => s.key !== 'all');
if (activeStage.value === 'all') {
// 全部Tab显示所有实际阶段列线索、谈判中、方案、赢单
return actualStages;
}
// 只显示选中阶段的列
return actualStages.filter(s => s.key === activeStage.value);
});
// ============== 新建商机模块 ==============
const showAddOpportunityDialog = ref(false);
const opportunityForm = ref<CrmOpportunityBo>({
dealerId: 0,
opportunityName: '',
stage: 'lead',
amount: 0,
probability: 10,
expectedCloseDate: '',
ownerUserId: 0,
productName: '',
description: '',
});
const dealerList = ref<CrmDealerVo[]>([]);
const dealerLoading = ref(false);
const userList = ref<UserInfo[]>([]);
const userLoading = ref(false);
// 打开新建商机Dialog
function openAddOpportunityDialog(stage?: string) {
// 根据点击位置自动设置商机阶段
// 如果在特定阶段的列中点击,自动设置该阶段
// 如果在全部列或toolbar按钮点击默认设置为"线索"
const defaultStage = stage && stage !== 'all' ? stage : 'lead';
opportunityForm.value = {
dealerId: 0,
opportunityName: '',
stage: defaultStage,
amount: 0,
probability: getProbabilityByStage(defaultStage),
expectedCloseDate: '',
ownerUserId: 0,
productName: '',
description: '',
};
showAddOpportunityDialog.value = true;
loadDealerList();
loadUserList();
}
// 根据阶段获取默认概率
function getProbabilityByStage(stage: string): number {
const probabilityMap: Record<string, number> = {
lead: 10,
negotiation: 30,
proposal: 50,
closing: 90,
};
return probabilityMap[stage] || 10;
}
// 加载经销商列表
async function loadDealerList(keyword?: string) {
dealerLoading.value = true;
try {
const res = await getDealerSelectList(keyword);
dealerList.value = res.data || [];
}
catch (error: any) {
ElMessage.error(error?.message || '加载经销商列表失败');
}
finally {
dealerLoading.value = false;
}
}
// 加载用户列表
async function loadUserList(keyword?: string) {
userLoading.value = true;
try {
const res = await getUserSelectList(keyword);
userList.value = res.data || [];
}
catch (error: any) {
ElMessage.error(error?.message || '加载用户列表失败');
}
finally {
userLoading.value = false;
}
}
// 提交新建商机
async function submitOpportunity() {
if (!opportunityForm.value.dealerId) {
ElMessage.warning('请选择经销商');
return;
}
if (!opportunityForm.value.opportunityName) {
ElMessage.warning('请输入商机名称');
return;
}
try {
await createOpportunity(opportunityForm.value);
ElMessage.success('商机创建成功');
showAddOpportunityDialog.value = false;
loadOpportunities();
}
catch (error: any) {
ElMessage.error(error?.message || '创建商机失败');
}
}
// ============== 商机详情模块 ==============
const showDetailDialog = ref(false);
const currentOpportunity = ref<CrmOpportunityVo | null>(null);
// 打开商机详情
function openOpportunityDetail(opp: CrmOpportunityVo) {
currentOpportunity.value = opp;
showDetailDialog.value = true;
}
// ============== 商机编辑模块 ==============
const showEditDialog = ref(false);
const editForm = ref<CrmOpportunityBo>({
opportunityId: 0,
dealerId: 0,
opportunityName: '',
stage: '',
amount: 0,
probability: 0,
expectedCloseDate: '',
ownerUserId: 0,
productName: '',
description: '',
});
// 打开编辑Dialog
function openEditDialog() {
if (!currentOpportunity.value)
return;
editForm.value = {
opportunityId: currentOpportunity.value.opportunityId,
dealerId: currentOpportunity.value.dealerId,
opportunityName: currentOpportunity.value.opportunityName,
stage: currentOpportunity.value.stage,
amount: Number(currentOpportunity.value.amount) || 0,
probability: currentOpportunity.value.probability || 0,
expectedCloseDate: currentOpportunity.value.expectedCloseDate || '',
ownerUserId: currentOpportunity.value.ownerUserId || 0,
productName: currentOpportunity.value.productName || '',
description: currentOpportunity.value.description || '',
};
showDetailDialog.value = false;
showEditDialog.value = true;
loadDealerList();
loadUserList();
}
// 提交编辑
async function submitEdit() {
if (!editForm.value.opportunityName) {
ElMessage.warning('请输入商机名称');
return;
}
try {
await updateOpportunity(editForm.value);
ElMessage.success('商机更新成功');
showEditDialog.value = false;
loadOpportunities();
}
catch (error: any) {
ElMessage.error(error?.message || '更新商机失败');
}
}
// ============== 商机删除模块 ==============
async function handleDeleteOpportunity() {
if (!currentOpportunity.value)
return;
try {
await ElMessageBox.confirm(
`确定要删除商机"${currentOpportunity.value.opportunityName}"吗?删除后无法恢复。`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
},
);
await deleteOpportunity(currentOpportunity.value.opportunityId.toString());
ElMessage.success('商机删除成功');
showDetailDialog.value = false;
loadOpportunities();
}
catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error?.message || '删除商机失败');
}
}
}
onMounted(() => {
@@ -113,8 +452,15 @@ onMounted(() => {
</div>
</div>
<div class="toolbar-right">
<el-input placeholder="搜索商机..." clearable style="width: 220px" />
<el-button type="primary" round>
<el-input
v-model="searchKeyword"
placeholder="搜索商机..."
clearable
style="width: 220px"
@keyup.enter="() => {}"
@clear="() => {}"
/>
<el-button type="primary" round @click="() => openAddOpportunityDialog()">
<el-icon><Plus /></el-icon> 新建商机
</el-button>
</div>
@@ -122,7 +468,12 @@ onMounted(() => {
<!-- Pipeline View -->
<div v-loading="opportunityLoading" class="pipeline-view">
<div v-for="stage in pipeline" :key="stage.key" class="pipeline-col">
<!-- 根据activeStage过滤显示的列 -->
<div
v-for="stage in filteredPipeline"
:key="stage.key"
class="pipeline-col"
>
<div class="col-header">
<span class="col-title">
<span class="col-dot" :style="{ background: stage.color }" />
@@ -131,7 +482,7 @@ onMounted(() => {
<span class="col-count">{{ stage.count }}</span>
</div>
<div class="col-cards">
<div v-for="opp in getCardsForStage(stage.key)" :key="opp.opportunityId" class="opportunity-card">
<div v-for="opp in getCardsForStage(stage.key)" :key="opp.opportunityId" class="opportunity-card" @click="openOpportunityDetail(opp)">
<div class="opp-header">
<span class="opp-name">{{ opp.opportunityName }}</span>
<el-tag v-if="opp.stage === 'closing'" type="success" size="small" effect="plain" round>
@@ -159,7 +510,7 @@ onMounted(() => {
</div>
</div>
<div class="opp-footer">
<span class="opp-amount">{{ opp.amount ? `¥${opp.amount.toLocaleString()}` : '--' }}</span>
<span class="opp-amount">{{ opp.amount ? `¥${Number(opp.amount).toLocaleString()}` : '--' }}</span>
<div class="opp-progress">
<el-progress :percentage="opp.probability || 0" :stroke-width="4" :show-text="false" :color="stage.color" />
</div>
@@ -170,7 +521,7 @@ onMounted(() => {
</div>
</div>
</div>
<div class="add-card-placeholder">
<div class="add-card-placeholder" @click="() => openAddOpportunityDialog(stage.key)">
<el-icon :size="20">
<Plus />
</el-icon>
@@ -179,6 +530,277 @@ onMounted(() => {
</div>
</div>
</div>
<!-- 新建商机Dialog -->
<el-dialog v-model="showAddOpportunityDialog" title="新建商机" width="600px" :close-on-click-modal="false">
<el-alert type="info" :closable="false" style="margin-bottom: 16px">
为经销商创建商机记录具体的销售机会
</el-alert>
<el-form :model="opportunityForm" label-width="120px">
<el-form-item label="经销商" required>
<el-select
v-model="opportunityForm.dealerId"
placeholder="请选择经销商"
filterable
:loading="dealerLoading"
style="width: 100%"
@filter-change="(keyword: string) => loadDealerList(keyword)"
>
<el-option
v-for="dealer in dealerList"
:key="dealer.dealerId"
:label="`${dealer.dealerName}${dealer.dealerCode}`"
:value="dealer.dealerId"
>
<div style="display: flex; align-items: center; justify-content: space-between">
<span>{{ dealer.dealerName }}</span>
<span style="color: #8492a6; font-size: 13px">{{ dealer.dealerCode }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="商机名称" required>
<el-input v-model="opportunityForm.opportunityName" placeholder="请输入商机名称首批进货订单" />
</el-form-item>
<el-form-item label="商机阶段">
<el-select v-model="opportunityForm.stage" style="width: 100%">
<el-option label="线索" value="lead" />
<el-option label="谈判中" value="negotiation" />
<el-option label="方案" value="proposal" />
<el-option label="赢单" value="closing" />
</el-select>
</el-form-item>
<el-form-item label="商机金额">
<el-input-number v-model="opportunityForm.amount" :min="0" :precision="2" style="width: 100%" />
</el-form-item>
<el-form-item label="成功概率">
<el-slider v-model="opportunityForm.probability" :min="0" :max="100" :step="5" show-input />
</el-form-item>
<el-form-item label="预计成交日期">
<el-date-picker
v-model="opportunityForm.expectedCloseDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择预计成交日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="负责人">
<el-select
v-model="opportunityForm.ownerUserId"
placeholder="请选择负责人"
filterable
:loading="userLoading"
style="width: 100%"
@filter-change="(keyword: string) => loadUserList(keyword)"
>
<el-option
v-for="user in userList"
:key="user.userId"
:label="`${user.nickName}${user.userName}`"
:value="user.userId"
>
<div style="display: flex; align-items: center; justify-content: space-between">
<span>{{ user.nickName }}</span>
<span v-if="user.deptName" style="color: #8492a6; font-size: 13px">{{ user.deptName }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="产品名称">
<el-input v-model="opportunityForm.productName" placeholder="请输入产品名称可选" />
</el-form-item>
<el-form-item label="商机描述">
<el-input v-model="opportunityForm.description" type="textarea" :rows="3" placeholder="请输入商机描述可选" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddOpportunityDialog = false">
取消
</el-button>
<el-button type="primary" @click="submitOpportunity">
创建商机
</el-button>
</template>
</el-dialog>
<!-- 商机详情Dialog -->
<el-dialog v-model="showDetailDialog" title="商机详情" width="600px" :close-on-click-modal="false">
<el-form label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="商机名称">
<span>{{ currentOpportunity?.opportunityName || '--' }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="经销商">
<span>{{ currentOpportunity?.dealerName || '--' }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="商机阶段">
<el-tag :type="currentOpportunity?.stage === 'closing' ? 'success' : currentOpportunity?.stage === 'lead' ? 'info' : 'warning'" size="small">
{{ currentOpportunity?.stageName || currentOpportunity?.stage || '--' }}
</el-tag>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-tag type="primary" size="small" effect="plain">
{{ currentOpportunity?.statusName || '进行中' }}
</el-tag>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="商机金额">
<span class="amount-value">{{ currentOpportunity?.amount ? `¥${Number(currentOpportunity.amount).toLocaleString()}` : '--' }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="成功概率">
<el-progress :percentage="currentOpportunity?.probability || 0" :stroke-width="6" style="width: 120px" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="负责人">
<span>{{ currentOpportunity?.ownerUserName || '--' }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预计成交日期">
<span>{{ currentOpportunity?.expectedCloseDate || '--' }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="产品名称">
<span>{{ currentOpportunity?.productName || '--' }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="创建时间">
<span>{{ currentOpportunity?.createTime || '--' }}</span>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="商机描述">
<span>{{ currentOpportunity?.description || '--' }}</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showDetailDialog = false">
关闭
</el-button>
<el-button type="primary" @click="openEditDialog">
<el-icon><Edit /></el-icon> 编辑
</el-button>
<el-button type="danger" @click="handleDeleteOpportunity">
<el-icon><Delete /></el-icon> 删除
</el-button>
</template>
</el-dialog>
<!-- 编辑商机Dialog -->
<el-dialog v-model="showEditDialog" title="编辑商机" width="600px" :close-on-click-modal="false">
<el-form :model="editForm" label-width="120px">
<el-form-item label="经销商" required>
<el-select
v-model="editForm.dealerId"
placeholder="请选择经销商"
filterable
:loading="dealerLoading"
style="width: 100%"
@filter-change="(keyword: string) => loadDealerList(keyword)"
>
<el-option
v-for="dealer in dealerList"
:key="dealer.dealerId"
:label="`${dealer.dealerName}${dealer.dealerCode}`"
:value="dealer.dealerId"
>
<div style="display: flex; align-items: center; justify-content: space-between">
<span>{{ dealer.dealerName }}</span>
<span style="color: #8492a6; font-size: 13px">{{ dealer.dealerCode }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="商机名称" required>
<el-input v-model="editForm.opportunityName" placeholder="请输入商机名称" />
</el-form-item>
<el-form-item label="商机阶段">
<el-select v-model="editForm.stage" style="width: 100%">
<el-option label="线索" value="lead" />
<el-option label="谈判中" value="negotiation" />
<el-option label="方案" value="proposal" />
<el-option label="赢单" value="closing" />
</el-select>
</el-form-item>
<el-form-item label="商机金额">
<el-input-number v-model="editForm.amount" :min="0" :precision="2" style="width: 100%" />
</el-form-item>
<el-form-item label="成功概率">
<el-slider v-model="editForm.probability" :min="0" :max="100" :step="5" show-input />
</el-form-item>
<el-form-item label="预计成交日期">
<el-date-picker
v-model="editForm.expectedCloseDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择预计成交日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="负责人">
<el-select
v-model="editForm.ownerUserId"
placeholder="请选择负责人"
filterable
:loading="userLoading"
style="width: 100%"
@filter-change="(keyword: string) => loadUserList(keyword)"
>
<el-option
v-for="user in userList"
:key="user.userId"
:label="`${user.nickName}${user.userName}`"
:value="user.userId"
>
<div style="display: flex; align-items: center; justify-content: space-between">
<span>{{ user.nickName }}</span>
<span v-if="user.deptName" style="color: #8492a6; font-size: 13px">{{ user.deptName }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="产品名称">
<el-input v-model="editForm.productName" placeholder="请输入产品名称" />
</el-form-item>
<el-form-item label="商机描述">
<el-input v-model="editForm.description" type="textarea" :rows="3" placeholder="请输入商机描述" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showEditDialog = false">
取消
</el-button>
<el-button type="primary" @click="submitEdit">
保存修改
</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -187,6 +809,12 @@ onMounted(() => {
padding: 20px;
}
.amount-value {
font-size: 16px;
font-weight: 600;
color: var(--color-accent);
}
.crm-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);

View File

@@ -0,0 +1,121 @@
-- CRM商机测试数据生成脚本
-- 使用方法: docker exec -i hzhub-mysql mysql -uroot -phzhub123 hzhub < test_opportunity_data.sql
-- ========================================
-- 1. 先创建经销商测试数据(如果需要)
-- ========================================
-- 使用INSERT IGNORE避免重复插入
INSERT IGNORE INTO crm_dealer (dealer_id, tenant_id, dealer_name, dealer_code, contact_name, mobile, province, city, level, lifecycle, owner_user_id, create_by, create_time, del_flag)
VALUES
(1001, '000000', '华东商贸有限公司', 'DL001', '张经理', '13800001001', '上海', '上海', 'A', 'active', 1, 1, NOW(), 0),
(1002, '000000', '华南供应链集团', 'DL002', '李总监', '13800001002', '广东', '广州', 'A', 'active', 1, 1, NOW(), 0),
(1003, '000000', '北方商贸股份', 'DL003', '王总', '13800001003', '北京', '北京', 'B', 'active', 2, 1, NOW(), 0),
(1004, '000000', '西南贸易公司', 'DL004', '陈经理', '13800001004', '四川', '成都', 'B', 'active', 2, 1, NOW(), 0),
(1005, '000000', '东北经销中心', 'DL005', '刘主管', '13800001005', '辽宁', '沈阳', 'C', 'active', 3, 1, NOW(), 0),
(1006, '000000', '中部物流商贸', 'DL006', '赵经理', '13800001006', '湖北', '武汉', 'B', 'active', 1, 1, NOW(), 0),
(1007, '000000', '沿海贸易集团', 'DL007', '孙总', '13800001007', '浙江', '杭州', 'A', 'active', 2, 1, NOW(), 0),
(1008, '000000', '西部商贸公司', 'DL008', '周经理', '13800001008', '陕西', '西安', 'C', 'active', 3, 1, NOW(), 0),
(1009, '000000', '南方快销商贸', 'DL009', '吴主管', '13800001009', '福建', '厦门', 'B', 'active', 1, 1, NOW(), 0),
(1010, '000000', '京津冀经销站', 'DL010', '郑经理', '13800001010', '河北', '石家庄', 'C', 'active', 2, 1, NOW(), 0);
-- ========================================
-- 2. 创建50条商机测试数据
-- ========================================
-- lead阶段 - 15条线索阶段新商机
INSERT IGNORE INTO crm_opportunity (opportunity_id, tenant_id, dealer_id, opportunity_name, stage, amount, probability, expected_close_date, owner_user_id, product_name, description, status, create_by, create_time, update_by, update_time, del_flag)
VALUES
(2001, '000000', 1001, '首批进货合作意向', 'lead', 50000.00, 10, DATE_ADD(NOW(), INTERVAL 90 DAY), 1, '智能货架系统', '客户对智能货架产品有兴趣,初步接触阶段', 'active', 1, DATE_ADD(NOW(), INTERVAL -5 DAY), 1, NOW(), 0),
(2002, '000000', 1002, '季度采购计划', 'lead', 120000.00, 10, DATE_ADD(NOW(), INTERVAL 85 DAY), 2, '仓储管理软件', '大型经销商季度采购需求', 'active', 1, DATE_ADD(NOW(), INTERVAL -3 DAY), 1, NOW(), 0),
(2003, '000000', 1003, '新品推广合作', 'lead', 35000.00, 10, DATE_ADD(NOW(), INTERVAL 80 DAY), 1, '营销推广服务', '新品上市推广合作意向', 'active', 2, NOW(), 2, NOW(), 0),
(2004, '000000', 1004, '门店升级项目', 'lead', 85000.00, 10, DATE_ADD(NOW(), INTERVAL 75 DAY), 2, '门店装修方案', '门店形象升级需求', 'active', 2, DATE_ADD(NOW(), INTERVAL -2 DAY), 2, NOW(), 0),
(2005, '000000', 1005, '区域代理申请', 'lead', 20000.00, 10, DATE_ADD(NOW(), INTERVAL 70 DAY), 3, '区域代理权', '申请区域代理资格', 'active', 3, NOW(), 3, NOW(), 0),
(2006, '000000', 1006, '物流系统对接', 'lead', 150000.00, 10, DATE_ADD(NOW(), INTERVAL 95 DAY), 1, '物流追踪系统', '物流管理系统对接需求', 'active', 1, DATE_ADD(NOW(), INTERVAL -7 DAY), 1, NOW(), 0),
(2007, '000000', 1007, '年度框架协议', 'lead', 300000.00, 10, DATE_ADD(NOW(), INTERVAL 100 DAY), 2, '综合服务包', '年度合作协议框架', 'active', 2, DATE_ADD(NOW(), INTERVAL -4 DAY), 2, NOW(), 0),
(2008, '000000', 1008, '本地化服务需求', 'lead', 45000.00, 10, DATE_ADD(NOW(), INTERVAL 65 DAY), 3, '本地运营支持', '本地化运营服务需求', 'active', 3, NOW(), 3, NOW(), 0),
(2009, '000000', 1009, '电商渠道拓展', 'lead', 75000.00, 10, DATE_ADD(NOW(), INTERVAL 60 DAY), 1, '电商平台对接', '电商销售渠道拓展', 'active', 1, DATE_ADD(NOW(), INTERVAL -1 DAY), 1, NOW(), 0),
(2010, '000000', 1010, '冷链仓储合作', 'lead', 180000.00, 10, DATE_ADD(NOW(), INTERVAL 88 DAY), 2, '冷链解决方案', '冷链仓储需求', 'active', 2, NOW(), 2, NOW(), 0),
(2011, '000000', 1001, '二次采购意向', 'lead', 60000.00, 10, DATE_ADD(NOW(), INTERVAL 92 DAY), 1, '货架配件', '已有合作客户追加采购', 'active', 1, NOW(), 1, NOW(), 0),
(2012, '000000', 1003, '促销活动支持', 'lead', 25000.00, 10, DATE_ADD(NOW(), INTERVAL 55 DAY), 2, '促销策划服务', '促销活动策划需求', 'active', 2, DATE_ADD(NOW(), INTERVAL -6 DAY), 2, NOW(), 0),
(2013, '000000', 1005, '培训服务需求', 'lead', 15000.00, 10, DATE_ADD(NOW(), INTERVAL 50 DAY), 3, '员工培训课程', '员工技能培训需求', 'active', 3, NOW(), 3, NOW(), 0),
(2014, '000000', 1007, '数据分析服务', 'lead', 40000.00, 10, DATE_ADD(NOW(), INTERVAL 45 DAY), 1, 'BI数据分析', '销售数据分析服务', 'active', 1, DATE_ADD(NOW(), INTERVAL -8 DAY), 1, NOW(), 0),
(2015, '000000', 1009, '客户管理系统', 'lead', 95000.00, 10, DATE_ADD(NOW(), INTERVAL 58 DAY), 2, 'CRM系统定制', '客户管理需求', 'active', 2, NOW(), 2, NOW(), 0);
-- negotiation阶段 - 15条谈判中
INSERT IGNORE INTO crm_opportunity (opportunity_id, tenant_id, dealer_id, opportunity_name, stage, amount, probability, expected_close_date, owner_user_id, product_name, description, status, create_by, create_time, update_by, update_time, del_flag)
VALUES
(2016, '000000', 1001, '大批量采购谈判', 'negotiation', 280000.00, 30, DATE_ADD(NOW(), INTERVAL 60 DAY), 1, '智能货架批量', '批量采购价格谈判', 'active', 1, DATE_ADD(NOW(), INTERVAL -20 DAY), 1, NOW(), 0),
(2017, '000000', 1002, '系统集成方案', 'negotiation', 450000.00, 30, DATE_ADD(NOW(), INTERVAL 55 DAY), 2, '全系统集成', '系统集成方案谈判', 'active', 2, DATE_ADD(NOW(), INTERVAL -18 DAY), 2, NOW(), 0),
(2018, '000000', 1003, '品牌合作推广', 'negotiation', 120000.00, 30, DATE_ADD(NOW(), INTERVAL 50 DAY), 1, '品牌联合推广', '品牌推广合作谈判', 'active', 1, DATE_ADD(NOW(), INTERVAL -15 DAY), 1, NOW(), 0),
(2019, '000000', 1004, '区域扩展计划', 'negotiation', 200000.00, 30, DATE_ADD(NOW(), INTERVAL 48 DAY), 2, '区域扩张服务', '区域扩展方案谈判', 'active', 2, DATE_ADD(NOW(), INTERVAL -12 DAY), 2, NOW(), 0),
(2020, '000000', 1005, '年度供货协议', 'negotiation', 350000.00, 30, DATE_ADD(NOW(), INTERVAL 45 DAY), 3, '年度供货合同', '年度协议谈判', 'active', 3, DATE_ADD(NOW(), INTERVAL -10 DAY), 3, NOW(), 0),
(2021, '000000', 1006, '智慧门店方案', 'negotiation', 180000.00, 30, DATE_ADD(NOW(), INTERVAL 42 DAY), 1, '智慧门店系统', '智慧门店方案谈判', 'active', 1, DATE_ADD(NOW(), INTERVAL -25 DAY), 1, NOW(), 0),
(2022, '000000', 1007, '营销自动化', 'negotiation', 95000.00, 30, DATE_ADD(NOW(), INTERVAL 40 DAY), 2, '营销自动化工具', '营销工具采购谈判', 'active', 2, DATE_ADD(NOW(), INTERVAL -22 DAY), 2, NOW(), 0),
(2023, '000000', 1008, '供应链优化', 'negotiation', 250000.00, 30, DATE_ADD(NOW(), INTERVAL 38 DAY), 3, '供应链优化方案', '供应链优化谈判', 'active', 3, DATE_ADD(NOW(), INTERVAL -19 DAY), 3, NOW(), 0),
(2024, '000000', 1009, '电商运营合作', 'negotiation', 150000.00, 30, DATE_ADD(NOW(), INTERVAL 35 DAY), 1, '电商运营服务', '电商运营合作谈判', 'active', 1, DATE_ADD(NOW(), INTERVAL -16 DAY), 1, NOW(), 0),
(2025, '000000', 1010, '仓储物流升级', 'negotiation', 320000.00, 30, DATE_ADD(NOW(), INTERVAL 33 DAY), 2, '仓储升级方案', '仓储升级谈判', 'active', 2, DATE_ADD(NOW(), INTERVAL -14 DAY), 2, NOW(), 0),
(2026, '000000', 1002, '增值服务采购', 'negotiation', 80000.00, 30, DATE_ADD(NOW(), INTERVAL 30 DAY), 2, '增值服务包', '增值服务采购谈判', 'active', 2, DATE_ADD(NOW(), INTERVAL -28 DAY), 2, NOW(), 0),
(2027, '000000', 1004, '定制开发需求', 'negotiation', 220000.00, 30, DATE_ADD(NOW(), INTERVAL 28 DAY), 1, '定制开发服务', '定制开发方案谈判', 'active', 1, DATE_ADD(NOW(), INTERVAL -21 DAY), 1, NOW(), 0),
(2028, '000000', 1006, '设备租赁合作', 'negotiation', 65000.00, 30, DATE_ADD(NOW(), INTERVAL 26 DAY), 3, '设备租赁方案', '设备租赁谈判', 'active', 3, DATE_ADD(NOW(), INTERVAL -17 DAY), 3, NOW(), 0),
(2029, '000000', 1008, '数据服务合作', 'negotiation', 100000.00, 30, DATE_ADD(NOW(), INTERVAL 24 DAY), 2, '数据分析服务', '数据服务谈判', 'active', 2, DATE_ADD(NOW(), INTERVAL -11 DAY), 2, NOW(), 0),
(2030, '000000', 1010, '技术支持服务', 'negotiation', 55000.00, 30, DATE_ADD(NOW(), INTERVAL 22 DAY), 1, '技术支持包', '技术支持谈判', 'active', 1, DATE_ADD(NOW(), INTERVAL -9 DAY), 1, NOW(), 0);
-- proposal阶段 - 12条方案阶段
INSERT IGNORE INTO crm_opportunity (opportunity_id, tenant_id, dealer_id, opportunity_name, stage, amount, probability, expected_close_date, owner_user_id, product_name, description, status, create_by, create_time, update_by, update_time, del_flag)
VALUES
(2031, '000000', 1001, '大型项目方案', 'proposal', 580000.00, 50, DATE_ADD(NOW(), INTERVAL 30 DAY), 1, '全链条解决方案', '已提交方案等待审批', 'active', 1, DATE_ADD(NOW(), INTERVAL -35 DAY), 1, NOW(), 0),
(2032, '000000', 1002, '年度战略合作', 'proposal', 850000.00, 50, DATE_ADD(NOW(), INTERVAL 28 DAY), 2, '战略合作方案', '方案已提交等待确认', 'active', 2, DATE_ADD(NOW(), INTERVAL -32 DAY), 2, NOW(), 0),
(2033, '000000', 1003, '智慧零售方案', 'proposal', 380000.00, 50, DATE_ADD(NOW(), INTERVAL 25 DAY), 1, '智慧零售系统', '方案评审中', 'active', 1, DATE_ADD(NOW(), INTERVAL -30 DAY), 1, NOW(), 0),
(2034, '000000', 1004, '区域代理方案', 'proposal', 250000.00, 50, DATE_ADD(NOW(), INTERVAL 23 DAY), 2, '代理合作方案', '代理方案已提交', 'active', 2, DATE_ADD(NOW(), INTERVAL -27 DAY), 2, NOW(), 0),
(2035, '000000', 1005, '仓储改造方案', 'proposal', 420000.00, 50, DATE_ADD(NOW(), INTERVAL 20 DAY), 3, '仓储改造计划', '改造方案等待审批', 'active', 3, DATE_ADD(NOW(), INTERVAL -25 DAY), 3, NOW(), 0),
(2036, '000000', 1006, '品牌升级方案', 'proposal', 180000.00, 50, DATE_ADD(NOW(), INTERVAL 18 DAY), 1, '品牌升级服务', '品牌方案评审中', 'active', 1, DATE_ADD(NOW(), INTERVAL -40 DAY), 1, NOW(), 0),
(2037, '000000', 1007, '营销整合方案', 'proposal', 320000.00, 50, DATE_ADD(NOW(), INTERVAL 15 DAY), 2, '营销整合服务', '营销方案已提交', 'active', 2, DATE_ADD(NOW(), INTERVAL -38 DAY), 2, NOW(), 0),
(2038, '000000', 1008, '供应链方案', 'proposal', 450000.00, 50, DATE_ADD(NOW(), INTERVAL 12 DAY), 3, '供应链整合', '供应链方案等待审批', 'active', 3, DATE_ADD(NOW(), INTERVAL -36 DAY), 3, NOW(), 0),
(2039, '000000', 1009, '电商平台方案', 'proposal', 280000.00, 50, DATE_ADD(NOW(), INTERVAL 10 DAY), 1, '电商全案', '电商方案评审中', 'active', 1, DATE_ADD(NOW(), INTERVAL -33 DAY), 1, NOW(), 0),
(2040, '000000', 1010, '物流优化方案', 'proposal', 350000.00, 50, DATE_ADD(NOW(), INTERVAL 8 DAY), 2, '物流优化服务', '物流方案已提交', 'active', 2, DATE_ADD(NOW(), INTERVAL -29 DAY), 2, NOW(), 0),
(2041, '000000', 1003, '培训体系建设', 'proposal', 120000.00, 50, DATE_ADD(NOW(), INTERVAL 15 DAY), 1, '培训体系建设', '培训方案等待审批', 'active', 1, DATE_ADD(NOW(), INTERVAL -42 DAY), 1, NOW(), 0),
(2042, '000000', 1007, '客户运营方案', 'proposal', 200000.00, 50, DATE_ADD(NOW(), INTERVAL 12 DAY), 2, '客户运营服务', '运营方案评审中', 'active', 2, DATE_ADD(NOW(), INTERVAL -44 DAY), 2, NOW(), 0);
-- closing阶段 - 8条赢单阶段本月和上月分布
INSERT IGNORE INTO crm_opportunity (opportunity_id, tenant_id, dealer_id, opportunity_name, stage, amount, probability, expected_close_date, owner_user_id, product_name, description, status, create_by, create_time, update_by, update_time, del_flag)
VALUES
(2043, '000000', 1001, '首批进货合同', 'closing', 680000.00, 90, DATE_ADD(NOW(), INTERVAL 5 DAY), 1, '智能货架系统', '合同已签订,准备执行', 'active', 1, DATE_ADD(NOW(), INTERVAL -45 DAY), 1, NOW(), 0),
(2044, '000000', 1002, '年度框架合同', 'closing', 1200000.00, 90, NOW(), 2, '综合服务包', '年度合同已签订', 'active', 2, DATE_ADD(NOW(), INTERVAL -50 DAY), 2, NOW(), 0),
(2045, '000000', 1003, '区域代理合同', 'closing', 350000.00, 90, DATE_ADD(NOW(), INTERVAL -3 DAY), 1, '区域代理权', '代理合同已签订', 'active', 1, DATE_ADD(NOW(), INTERVAL -48 DAY), 1, NOW(), 0),
(2046, '000000', 1004, '仓储改造合同', 'closing', 520000.00, 90, DATE_ADD(NOW(), INTERVAL -7 DAY), 2, '仓储改造计划', '改造合同已签订', 'active', 2, DATE_ADD(NOW(), INTERVAL -52 DAY), 2, NOW(), 0),
-- 上月赢单(用于计算环比)
(2047, '000000', 1005, '上月签约项目A', 'closing', 450000.00, 90, DATE_ADD(NOW(), INTERVAL -35 DAY), 3, '物流方案', '上月赢单案例', 'active', 3, DATE_ADD(NOW(), INTERVAL -55 DAY), 3, NOW(), 0),
(2048, '000000', 1006, '上月签约项目B', 'closing', 280000.00, 90, DATE_ADD(NOW(), INTERVAL -38 DAY), 1, '营销服务', '上月赢单案例', 'active', 1, DATE_ADD(NOW(), INTERVAL -58 DAY), 1, NOW(), 0),
(2049, '000000', 1007, '上月签约项目C', 'closing', 380000.00, 90, DATE_ADD(NOW(), INTERVAL -40 DAY), 2, '数据服务', '上月赢单案例', 'active', 2, DATE_ADD(NOW(), INTERVAL -62 DAY), 2, NOW(), 0),
(2050, '000000', 1008, '上月签约项目D', 'closing', 150000.00, 90, DATE_ADD(NOW(), INTERVAL -42 DAY), 3, '培训服务', '上月赢单案例', 'active', 3, DATE_ADD(NOW(), INTERVAL -65 DAY), 3, NOW(), 0);
-- ========================================
-- 3. 验证数据插入结果
-- ========================================
SELECT '=== 各阶段商机统计 ===' as title;
SELECT
stage,
COUNT(*) as count,
SUM(amount) as total_amount,
ROUND(AVG(amount), 2) as avg_amount
FROM crm_opportunity
WHERE del_flag = 0
GROUP BY stage
ORDER BY FIELD(stage, 'lead', 'negotiation', 'proposal', 'closing');
SELECT '=== 赢单月度统计 ===' as title;
SELECT
CASE
WHEN MONTH(create_time) = MONTH(NOW()) AND YEAR(create_time) = YEAR(NOW()) THEN '本月'
WHEN MONTH(create_time) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND YEAR(create_time) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) THEN '上月'
ELSE '其他'
END as month_period,
COUNT(*) as win_count,
SUM(amount) as win_amount
FROM crm_opportunity
WHERE stage = 'closing' AND del_flag = 0
GROUP BY month_period;
SELECT '=== 经销商数据确认 ===' as title;
SELECT dealer_id, dealer_name, dealer_code FROM crm_dealer WHERE del_flag = 0 LIMIT 10;

View File

@@ -0,0 +1,36 @@
package org.hzhub.crm.controller;
import lombok.RequiredArgsConstructor;
import org.hzhub.common.core.domain.R;
import org.hzhub.crm.domain.vo.CrmDealerVo;
import org.hzhub.crm.service.ICrmDealerService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* CRM经销商管理 Controller
* 员工门户版本无需Sa-Token权限注解权限由Gateway控制
*
* 注意Gateway已配置 /crm/** 路由并StripPrefix=1去除/crm前缀
* 所以Controller使用 /dealer 路径,实际对外接口为 /crm/dealer/**
*
* @author hzhub
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/dealer")
public class CrmDealerController {
private final ICrmDealerService dealerService;
/**
* 员工门户经销商选择器列表
* 用于商机创建、商机分配等场景
*/
@GetMapping("/portal/select")
public R<List<CrmDealerVo>> portalSelect(@RequestParam(required = false) String keyword) {
List<CrmDealerVo> dealerList = dealerService.selectDealerList(keyword);
return R.ok(dealerList);
}
}

View File

@@ -5,6 +5,8 @@ import org.hzhub.common.mybatis.core.page.TableDataInfo;
import org.hzhub.crm.domain.bo.CrmDealerBo;
import org.hzhub.crm.domain.vo.CrmDealerVo;
import java.util.List;
/**
* CRM经销商 Service 接口
*
@@ -60,4 +62,12 @@ public interface ICrmDealerService {
* @return 结果
*/
boolean checkDealerCodeUnique(CrmDealerBo dealer);
/**
* 查询经销商列表(用于选择器)
*
* @param keyword 搜索关键词(经销商名称/编码)
* @return 经销商列表
*/
List<CrmDealerVo> selectDealerList(String keyword);
}

View File

@@ -106,4 +106,18 @@ public class CrmDealerServiceImpl implements ICrmDealerService {
.ne(ObjectUtil.isNotNull(dealer.getDealerId()), CrmDealer::getDealerId, dealer.getDealerId()));
return !exist;
}
@Override
public List<CrmDealerVo> selectDealerList(String keyword) {
LambdaQueryWrapper<CrmDealer> wrapper = Wrappers.lambdaQuery();
wrapper.eq(CrmDealer::getDelFlag, SystemConstants.NORMAL)
.eq(CrmDealer::getLifecycle, "active") // 只返回活跃状态的经销商
.and(StringUtils.isNotBlank(keyword), w -> w
.like(CrmDealer::getDealerName, keyword)
.or()
.like(CrmDealer::getDealerCode, keyword))
.orderByDesc(CrmDealer::getCreateTime)
.last("LIMIT 100"); // 限制返回100条
return dealerMapper.selectVoList(wrapper);
}
}

View File

@@ -290,22 +290,12 @@ public class CrmLeadServiceImpl implements ICrmLeadService {
throw new ServiceException("创建经销商失败");
}
// 4. 创建初始商机(线索转化时自动创建)
int opportunityResult = opportunityService.createInitialOpportunity(
dealer.getDealerId(),
lead.getLeadId(),
lead.getOwnerUserId()
);
if (opportunityResult <= 0) {
log.warn("创建初始商机失败,但经销商创建成功: dealerId={}", dealer.getDealerId());
}
// 5. 更新线索状态
// 4. 更新线索状态
lead.setLeadStatus("converted");
lead.setConvertedDealerId(dealer.getDealerId());
int leadResult = leadMapper.updateById(lead);
log.info("线索转化成功: leadId={}, dealerId={}, dealerCode={}, opportunityId已创建",
log.info("线索转化成功: leadId={}, dealerId={}, dealerCode={}",
lead.getLeadId(), dealer.getDealerId(), dealer.getDealerCode());
return leadResult;