模块: 04-商机管理
版本: v1.0
日期: 2025-10-03
关联文档:
- 需求文档:
docs/04-商机管理模块需求.md- 原型图:
yuanxing/04-商机管理原型图.md- 数据库:
sql/crm/04-crm_opportunity.sql
接口地址: GET /crm/opportunity/list
接口描述: 分页查询商机列表,支持多条件筛选
权限标识: crm:opportunity:list
请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | pageNum | Integer | 否 | 页码,默认1 | | pageSize | Integer | 否 | 每页条数,默认10 | | opportunityName | String | 否 | 商机名称(模糊查询) | | opportunityNo | String | 否 | 商机编号(模糊查询) | | customerId | Long | 否 | 客户ID | | customerName | String | 否 | 客户名称(模糊查询) | | stage | String | 否 | 商机阶段 | | status | String | 否 | 商机状态 | | ownerUserId | Long | 否 | 负责人ID | | minAmount | BigDecimal | 否 | 最小金额 | | maxAmount | BigDecimal | 否 | 最大金额 | | source | String | 否 | 商机来源 | | expectDealDateBegin | String | 否 | 预计成交日期开始 | | expectDealDateEnd | String | 否 | 预计成交日期结束 | | beginTime | String | 否 | 创建开始时间 | | endTime | String | 否 | 创建结束时间 |
返回示例:
{
"code": 200,
"msg": "查询成功",
"rows": [
{
"id": 1,
"opportunityNo": "OPP20250101001",
"opportunityName": "华为CRM系统采购项目",
"customerId": 1,
"customerName": "华为技术有限公司",
"contactId": 1,
"contactName": "张伟",
"amount": 150000.00,
"stage": "quote",
"stagePercent": 60,
"status": "following",
"winRate": 75.00,
"expectDealDate": "2025-03-31",
"actualDealDate": null,
"ownerUserId": 1,
"ownerUserName": "张三",
"productCount": 2,
"lastTime": "2025-01-02 10:30:00",
"nextTime": "2025-01-05 14:00:00",
"followCount": 8,
"contractId": null,
"source": "website",
"createTime": "2024-12-01 09:00:00",
"remark": ""
}
],
"total": 89
}
接口地址: GET /crm/opportunity/{id}
接口描述: 根据ID查询商机详细信息
权限标识: crm:opportunity:query
请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | id | Long | 是 | 商机ID |
返回示例: 同1.1列表返回的单条数据,包含完整字段
接口地址: POST /crm/opportunity
接口描述: 新增商机信息
权限标识: crm:opportunity:add
请求体:
{
"opportunityNo": "OPP20250101001",
"opportunityName": "华为CRM系统采购项目",
"customerId": 1,
"contactId": 1,
"amount": 150000.00,
"stage": "initial",
"expectDealDate": "2025-03-31",
"winRate": 50.00,
"ownerUserId": 1,
"source": "website",
"nextTime": "2025-01-05 14:00:00",
"remark": ""
}
请求参数说明: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | opportunityNo | String | 是 | 商机编号,唯一,格式:OPP+yyyyMMdd+4位流水号 | | opportunityName | String | 是 | 商机名称,最大200字符 | | customerId | Long | 是 | 客户ID | | contactId | Long | 否 | 联系人ID | | amount | BigDecimal | 是 | 商机金额,保留2位小数 | | stage | String | 否 | 商机阶段,默认initial | | expectDealDate | String | 否 | 预计成交日期,格式yyyy-MM-dd | | winRate | BigDecimal | 否 | 赢单率,百分比,默认50.00 | | ownerUserId | Long | 否 | 负责人ID,默认当前用户 | | source | String | 否 | 商机来源 | | nextTime | String | 否 | 下次联系时间 yyyy-MM-dd HH:mm:ss | | remark | String | 否 | 备注,最大500字符 |
校验规则:
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: PUT /crm/opportunity
接口描述: 修改商机信息
权限标识: crm:opportunity:edit
请求体: 同新增接口,但需要传入id
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: DELETE /crm/opportunity/{ids}
接口描述: 删除商机(支持批量)
权限标识: crm:opportunity:remove
请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | ids | Long[] | 是 | 商机ID数组 |
业务规则:
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: PUT /crm/opportunity/stage
接口描述: 修改商机阶段,记录阶段日志
权限标识: crm:opportunity:edit
请求体:
{
"id": 1,
"stage": "quote",
"stagePercent": 60,
"winRate": 75.00,
"changeReason": "客户已确认需求,准备报价",
"remark": ""
}
请求参数说明: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | id | Long | 是 | 商机ID | | stage | String | 是 | 新阶段:initial/requirement/solution/quote/negotiate/contract/won | | stagePercent | Integer | 是 | 阶段进度:10/20/40/60/80/90/100 | | winRate | BigDecimal | 否 | 赢单率 | | changeReason | String | 否 | 变更原因 | | remark | String | 否 | 备注 |
业务规则:
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: PUT /crm/opportunity/win
接口描述: 将商机标记为赢单
权限标识: crm:opportunity:edit
请求体:
{
"id": 1,
"actualDealDate": "2025-01-10",
"actualAmount": 148000.00,
"remark": "成功签约"
}
请求参数说明: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | id | Long | 是 | 商机ID | | actualDealDate | String | 是 | 实际成交日期 yyyy-MM-dd | | actualAmount | BigDecimal | 否 | 实际成交金额(如果与预估不同) | | remark | String | 否 | 备注说明 |
业务规则:
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: PUT /crm/opportunity/lost
接口描述: 将商机标记为输单
权限标识: crm:opportunity:edit
请求体:
{
"id": 1,
"lostReason": "价格因素",
"remark": "客户选择了竞争对手的产品"
}
请求参数说明: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | id | Long | 是 | 商机ID | | lostReason | String | 是 | 输单原因,最大500字符 | | remark | String | 否 | 备注说明 |
业务规则:
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: PUT /crm/opportunity/assign
接口描述: 批量分配商机给指定负责人
权限标识: crm:opportunity:assign
请求体:
{
"ids": [1, 2, 3],
"ownerUserId": 5,
"remark": "分配原因"
}
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: POST /crm/opportunity/export
接口描述: 导出商机数据到Excel
权限标识: crm:opportunity:export
请求体: 查询参数同1.1列表查询接口
返回: Excel文件流
接口地址: GET /crm/opportunity/product/list
接口描述: 查询指定商机的产品列表
权限标识: crm:opportunity:query
请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | opportunityId | Long | 是 | 商机ID |
返回示例:
{
"code": 200,
"msg": "查询成功",
"data": [
{
"id": 1,
"opportunityId": 1,
"productId": 1,
"productName": "CRM系统标准版",
"productNo": "PRD20250101001",
"categoryId": 11,
"categoryName": "CRM系统",
"model": "V5.4.0",
"unit": "套",
"quantity": 1,
"price": 12800.00,
"discount": 10.00,
"subtotal": 11520.00,
"sort": 1,
"remark": ""
}
]
}
接口地址: POST /crm/opportunity/product
接口描述: 向商机添加产品
权限标识: crm:opportunity:edit
请求体:
{
"opportunityId": 1,
"productId": 1,
"quantity": 1,
"price": 12800.00,
"discount": 10.00,
"remark": ""
}
请求参数说明: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | opportunityId | Long | 是 | 商机ID | | productId | Long | 是 | 产品ID | | quantity | Integer | 是 | 数量,必须大于0 | | price | BigDecimal | 是 | 单价,保留2位小数 | | discount | BigDecimal | 否 | 折扣百分比,默认0 | | remark | String | 否 | 备注 |
业务规则:
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: PUT /crm/opportunity/product
接口描述: 修改商机产品信息
权限标识: crm:opportunity:edit
请求体: 同添加接口,但需要传入id
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: DELETE /crm/opportunity/product/{ids}
接口描述: 删除商机产品(支持批量)
权限标识: crm:opportunity:edit
请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | ids | Long[] | 是 | 商机产品ID数组 |
业务规则:
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: GET /crm/opportunity/follow/list
接口描述: 查询指定商机的跟进记录列表
权限标识: crm:opportunity:query
请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | opportunityId | Long | 是 | 商机ID | | pageNum | Integer | 否 | 页码,默认1 | | pageSize | Integer | 否 | 每页条数,默认10 |
返回示例:
{
"code": 200,
"msg": "查询成功",
"rows": [
{
"id": 1,
"opportunityId": 1,
"followType": "phone",
"followContent": "电话沟通了客户需求,客户对CRM系统很感兴趣",
"stage": "requirement",
"stagePercent": 20,
"nextTime": "2025-01-08 14:00:00",
"nextContent": "下次需要进行产品演示",
"attachments": "/upload/2025/01/file1.pdf",
"contactId": 1,
"contactName": "张伟",
"location": null,
"duration": 30,
"createBy": 1,
"createByName": "张三",
"createTime": "2025-01-05 10:30:00",
"remark": ""
}
],
"total": 8
}
接口地址: POST /crm/opportunity/follow
接口描述: 新增商机跟进记录
权限标识: crm:opportunity:follow
请求体:
{
"opportunityId": 1,
"followType": "phone",
"followContent": "电话沟通了客户需求",
"stage": "requirement",
"stagePercent": 20,
"nextTime": "2025-01-08 14:00:00",
"nextContent": "下次需要进行产品演示",
"attachments": "/upload/2025/01/file1.pdf",
"contactId": 1,
"duration": 30,
"remark": ""
}
请求参数说明: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | opportunityId | Long | 是 | 商机ID | | followType | String | 是 | 跟进方式:phone/wechat/email/visit/meeting/other | | followContent | String | 是 | 跟进内容,文本类型 | | stage | String | 否 | 当前商机阶段 | | stagePercent | Integer | 否 | 阶段进度百分比 | | nextTime | String | 否 | 下次联系时间 yyyy-MM-dd HH:mm:ss | | nextContent | String | 否 | 下次联系计划,最大500字符 | | attachments | String | 否 | 附件路径,多个用逗号分隔 | | contactId | Long | 否 | 联系人ID | | location | String | 否 | 跟进地点(拜访时填写) | | duration | Integer | 否 | 跟进时长(分钟) | | remark | String | 否 | 备注 |
业务规则:
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: PUT /crm/opportunity/follow
接口描述: 修改跟进记录信息
权限标识: crm:opportunity:follow
请求体: 同新增接口,但需要传入id
业务规则:
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: DELETE /crm/opportunity/follow/{ids}
接口描述: 删除跟进记录(支持批量)
权限标识: crm:opportunity:follow
请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | ids | Long[] | 是 | 跟进记录ID数组 |
业务规则:
返回示例:
{
"code": 200,
"msg": "操作成功"
}
接口地址: GET /crm/opportunity/stageLog/list
接口描述: 查询商机的阶段变更历史
权限标识: crm:opportunity:query
请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | opportunityId | Long | 是 | 商机ID |
返回示例:
{
"code": 200,
"msg": "查询成功",
"data": [
{
"id": 1,
"opportunityId": 1,
"fromStage": "initial",
"fromStageName": "初步接触",
"fromPercent": 10,
"toStage": "requirement",
"toStageName": "需求分析",
"toPercent": 20,
"changeType": "forward",
"changeReason": "客户明确了需求",
"changeAmount": 150000.00,
"createBy": 1,
"createByName": "张三",
"createTime": "2024-12-10 14:30:00",
"remark": ""
}
]
}
接口地址: GET /crm/opportunity/statistics/overview
接口描述: 获取商机统计概览数据
权限标识: crm:opportunity:list
请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | ownerUserId | Long | 否 | 负责人ID,不传则查询当前用户 | | beginTime | String | 否 | 开始时间 | | endTime | String | 否 | 结束时间 |
返回示例:
{
"code": 200,
"msg": "查询成功",
"data": {
"totalCount": 89,
"followingCount": 67,
"wonCount": 15,
"lostCount": 7,
"totalAmount": 5680000.00,
"wonAmount": 1250000.00,
"avgAmount": 63820.22,
"winRate": 68.18,
"avgCycle": 45.5
}
}
接口地址: GET /crm/opportunity/statistics/stage
接口描述: 按商机阶段统计分布情况
权限标识: crm:opportunity:list
请求参数: 无
返回示例:
{
"code": 200,
"msg": "查询成功",
"data": [
{
"stage": "initial",
"stageName": "初步接触",
"count": 12,
"amount": 560000.00,
"percent": 13.48
},
{
"stage": "requirement",
"stageName": "需求分析",
"count": 18,
"amount": 980000.00,
"percent": 20.22
}
]
}
接口地址: GET /crm/opportunity/statistics/winRate
接口描述: 分析商机赢单率趋势
权限标识: crm:opportunity:list
请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | type | String | 否 | 统计类型:month/quarter/year,默认month | | beginTime | String | 否 | 开始时间 | | endTime | String | 否 | 结束时间 |
返回示例:
{
"code": 200,
"msg": "查询成功",
"data": [
{
"period": "2025-01",
"totalCount": 23,
"wonCount": 15,
"lostCount": 8,
"winRate": 65.22,
"wonAmount": 1250000.00
}
]
}
接口地址: GET /crm/opportunity/statistics/owner
接口描述: 按负责人统计商机数量和金额
权限标识: crm:opportunity:list
请求参数: | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | beginTime | String | 否 | 开始时间 | | endTime | String | 否 | 结束时间 |
返回示例:
{
"code": 200,
"msg": "查询成功",
"data": [
{
"ownerUserId": 1,
"ownerUserName": "张三",
"totalCount": 28,
"followingCount": 20,
"wonCount": 6,
"lostCount": 2,
"totalAmount": 1850000.00,
"wonAmount": 650000.00,
"winRate": 75.00
}
]
}
public class CrmOpportunityBo extends BaseEntity {
/** 主键ID */
@NotNull(message = "主键不能为空", groups = { EditGroup.class })
private Long id;
/** 商机编号 */
@NotBlank(message = "商机编号不能为空", groups = { AddGroup.class, EditGroup.class })
private String opportunityNo;
/** 商机名称 */
@NotBlank(message = "商机名称不能为空", groups = { AddGroup.class, EditGroup.class })
private String opportunityName;
/** 客户ID */
@NotNull(message = "客户ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long customerId;
/** 联系人ID */
private Long contactId;
/** 商机金额 */
@NotNull(message = "商机金额不能为空", groups = { AddGroup.class, EditGroup.class })
@Min(value = 0, message = "商机金额必须大于0")
private BigDecimal amount;
/** 商机阶段 */
private String stage;
/** 阶段进度百分比 */
private Integer stagePercent;
/** 赢单率 */
private BigDecimal winRate;
/** 预计成交日期 */
private String expectDealDate;
/** 负责人ID */
private Long ownerUserId;
/** 商机来源 */
private String source;
/** 下次联系时间 */
private LocalDateTime nextTime;
/** 备注 */
private String remark;
}
public class CrmOpportunityVo implements Serializable {
/** 主键ID */
@ExcelProperty(value = "ID")
private Long id;
/** 商机编号 */
@ExcelProperty(value = "商机编号")
private String opportunityNo;
/** 商机名称 */
@ExcelProperty(value = "商机名称")
private String opportunityName;
/** 客户ID */
private Long customerId;
/** 客户名称 */
@ExcelProperty(value = "客户名称")
private String customerName;
/** 联系人ID */
private Long contactId;
/** 联系人姓名 */
@ExcelProperty(value = "联系人")
private String contactName;
/** 商机金额 */
@ExcelProperty(value = "商机金额")
private BigDecimal amount;
/** 商机阶段 */
@ExcelProperty(value = "商机阶段", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "crm_opportunity_stage")
private String stage;
/** 阶段进度百分比 */
@ExcelProperty(value = "阶段进度")
private Integer stagePercent;
/** 商机状态 */
@ExcelProperty(value = "商机状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "crm_opportunity_status")
private String status;
/** 赢单率 */
@ExcelProperty(value = "赢单率")
private BigDecimal winRate;
/** 预计成交日期 */
@ExcelProperty(value = "预计成交日期")
private String expectDealDate;
/** 实际成交日期 */
@ExcelProperty(value = "实际成交日期")
private String actualDealDate;
/** 输单原因 */
private String lostReason;
/** 负责人ID */
private Long ownerUserId;
/** 负责人姓名 */
@ExcelProperty(value = "负责人")
private String ownerUserName;
/** 产品数量 */
@ExcelProperty(value = "产品数量")
private Integer productCount;
/** 最后跟进时间 */
@ExcelProperty(value = "最后跟进时间")
private LocalDateTime lastTime;
/** 下次联系时间 */
@ExcelProperty(value = "下次联系时间")
private LocalDateTime nextTime;
/** 跟进次数 */
@ExcelProperty(value = "跟进次数")
private Integer followCount;
/** 关联合同ID */
private Long contractId;
/** 商机来源 */
@ExcelProperty(value = "商机来源", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "crm_clue_source")
private String source;
/** 创建时间 */
@ExcelProperty(value = "创建时间")
private LocalDateTime createTime;
/** 备注 */
@ExcelProperty(value = "备注")
private String remark;
}
| 字典值 | 字典标签 | 进度 | 说明 |
|---|---|---|---|
| initial | 初步接触 | 10% | 初次接触客户 |
| requirement | 需求分析 | 20% | 分析客户需求 |
| solution | 方案制定 | 40% | 制定解决方案 |
| quote | 报价 | 60% | 提供报价 |
| negotiate | 谈判审核 | 80% | 商务谈判 |
| contract | 合同签订 | 90% | 签订合同 |
| won | 赢单 | 100% | 成功赢单 |
| 字典值 | 字典标签 | 说明 |
|---|---|---|
| following | 跟进中 | 正在跟进 |
| won | 赢单 | 成功签约 |
| lost | 输单 | 失去商机 |
| invalid | 无效 | 无效商机 |
| 权限标识 | 说明 |
|---|---|
| crm:opportunity:list | 商机列表查询 |
| crm:opportunity:query | 商机详情查询 |
| crm:opportunity:add | 商机新增 |
| crm:opportunity:edit | 商机修改 |
| crm:opportunity:remove | 商机删除 |
| crm:opportunity:export | 商机导出 |
| crm:opportunity:assign | 商机分配 |
| crm:opportunity:follow | 添加跟进记录 |
文档结束