bugfix
This commit is contained in:
commit
1b0cc60236
83
README.md
Normal file
83
README.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# 商机管理模块 (Opportunity Management)
|
||||||
|
|
||||||
|
## 模块概述
|
||||||
|
商机管理模块提供完整的销售跟进和商机全生命周期管理功能,支持商机创建、阶段管理、漏斗分析和销售预测。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 2.1 商机管理(销售跟进模块)
|
||||||
|
|
||||||
|
#### 2.1.1 商机全生命周期管理
|
||||||
|
- **商机创建**:支持手动录入和线索转化,必填字段包括客户名称、预估金额、销售阶段、预计成交时间
|
||||||
|
- **阶段管理**:自定义销售漏斗(初步接洽→需求确认→方案报价→合同谈判→成交),阶段变更需记录原因
|
||||||
|
|
||||||
|
#### 2.1.2 商机分析
|
||||||
|
- **漏斗可视化**:展示各阶段商机数量/金额占比,支持按区域、销售维度筛选
|
||||||
|
- **预测功能**:基于历史转化率自动计算预计成交金额,偏差率≤15%
|
||||||
|
|
||||||
|
## 数据库表结构
|
||||||
|
|
||||||
|
### opportunities (商机表)
|
||||||
|
- `id`: 商机ID (主键)
|
||||||
|
- `customer_name`: 客户名称 (必填)
|
||||||
|
- `estimated_amount`: 预估金额 (必填)
|
||||||
|
- `current_stage`: 当前销售阶段 (必填)
|
||||||
|
- `expected_close_date`: 预计成交时间 (必填)
|
||||||
|
- `source_type`: 来源类型 (manual=手动录入, lead=线索转化)
|
||||||
|
- `owner_id`: 负责人ID
|
||||||
|
- `region`: 区域
|
||||||
|
- `created_at`: 创建时间
|
||||||
|
- `updated_at`: 更新时间
|
||||||
|
- `status`: 状态 (active=活跃, won=成交, lost=丢单)
|
||||||
|
|
||||||
|
### sales_stages (销售阶段表)
|
||||||
|
- `id`: 阶段ID (主键)
|
||||||
|
- `stage_name`: 阶段名称
|
||||||
|
- `stage_order`: 阶段顺序
|
||||||
|
- `description`: 阶段描述
|
||||||
|
- `conversion_rate`: 历史转化率
|
||||||
|
- `is_active`: 是否启用
|
||||||
|
|
||||||
|
### opportunity_stage_history (商机阶段变更历史表)
|
||||||
|
- `id`: 历史记录ID (主键)
|
||||||
|
- `opportunity_id`: 商机ID
|
||||||
|
- `from_stage`: 原阶段
|
||||||
|
- `to_stage`: 目标阶段
|
||||||
|
- `change_reason`: 变更原因 (必填)
|
||||||
|
- `changed_by`: 变更人ID
|
||||||
|
- `changed_at`: 变更时间
|
||||||
|
|
||||||
|
### opportunity_predictions (商机预测表)
|
||||||
|
- `id`: 预测记录ID (主键)
|
||||||
|
- `opportunity_id`: 商机ID
|
||||||
|
- `predicted_amount`: 预测金额
|
||||||
|
- `confidence_level`: 置信度
|
||||||
|
- `prediction_date`: 预测日期
|
||||||
|
- `actual_amount`: 实际金额 (成交后更新)
|
||||||
|
- `deviation_rate`: 偏差率
|
||||||
|
|
||||||
|
## API接口
|
||||||
|
|
||||||
|
### 商机管理
|
||||||
|
- `create_opportunity()`: 创建商机
|
||||||
|
- `update_opportunity_stage()`: 更新商机阶段
|
||||||
|
|
||||||
|
### 数据分析
|
||||||
|
- `get_funnel_analysis()`: 获取销售漏斗分析数据
|
||||||
|
- `get_sales_prediction()`: 获取销售预测汇总
|
||||||
|
|
||||||
|
## 前端界面
|
||||||
|
- 商机列表CRUD界面
|
||||||
|
- 销售阶段管理界面
|
||||||
|
- 销售漏斗可视化图表
|
||||||
|
- 销售预测汇总卡片
|
||||||
|
|
||||||
|
## 安装部署
|
||||||
|
1. 将模块目录复制到 `~/repos/` 目录下
|
||||||
|
2. 运行主应用的 `build.sh` 脚本,自动处理数据库表创建和前端资源链接
|
||||||
|
3. 模块将自动集成到系统中
|
||||||
|
|
||||||
|
## 依赖要求
|
||||||
|
- ahserver >= 1.0.0
|
||||||
|
- sqlor-database-module >= 1.0.0
|
||||||
|
- bricks-framework >= 1.0.0
|
||||||
54
init/data.json
Normal file
54
init/data.json
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"sales_stages": [
|
||||||
|
{
|
||||||
|
"id": "1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p",
|
||||||
|
"stage_name": "初步接洽",
|
||||||
|
"stage_order": 1,
|
||||||
|
"description": "与潜在客户初步接触,了解基本信息",
|
||||||
|
"conversion_rate": "0.8000",
|
||||||
|
"created_at": "2026-04-16 10:30:00",
|
||||||
|
"updated_at": "2026-04-16 10:30:00",
|
||||||
|
"is_active": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q",
|
||||||
|
"stage_name": "需求确认",
|
||||||
|
"stage_order": 2,
|
||||||
|
"description": "深入了解客户需求和痛点",
|
||||||
|
"conversion_rate": "0.7000",
|
||||||
|
"created_at": "2026-04-16 10:30:00",
|
||||||
|
"updated_at": "2026-04-16 10:30:00",
|
||||||
|
"is_active": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r",
|
||||||
|
"stage_name": "方案报价",
|
||||||
|
"stage_order": 3,
|
||||||
|
"description": "提供解决方案和报价",
|
||||||
|
"conversion_rate": "0.6000",
|
||||||
|
"created_at": "2026-04-16 10:30:00",
|
||||||
|
"updated_at": "2026-04-16 10:30:00",
|
||||||
|
"is_active": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s",
|
||||||
|
"stage_name": "合同谈判",
|
||||||
|
"stage_order": 4,
|
||||||
|
"description": "合同条款谈判和细节确认",
|
||||||
|
"conversion_rate": "0.5000",
|
||||||
|
"created_at": "2026-04-16 10:30:00",
|
||||||
|
"updated_at": "2026-04-16 10:30:00",
|
||||||
|
"is_active": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t",
|
||||||
|
"stage_name": "成交",
|
||||||
|
"stage_order": 5,
|
||||||
|
"description": "成功签约成交",
|
||||||
|
"conversion_rate": "1.0000",
|
||||||
|
"created_at": "2026-04-16 10:30:00",
|
||||||
|
"updated_at": "2026-04-16 10:30:00",
|
||||||
|
"is_active": "1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
82
json/funnel_analysis.json
Normal file
82
json/funnel_analysis.json
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"name": "funnel_analysis",
|
||||||
|
"title": "销售漏斗分析",
|
||||||
|
"type": "page",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "container",
|
||||||
|
"name": "filters",
|
||||||
|
"title": "筛选条件",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"name": "region",
|
||||||
|
"label": "区域",
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"name": "owner_id",
|
||||||
|
"label": "销售负责人",
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "date_range",
|
||||||
|
"name": "date_range",
|
||||||
|
"label": "时间范围"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button",
|
||||||
|
"name": "search",
|
||||||
|
"label": "查询",
|
||||||
|
"action": "refresh_funnel_data"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chart",
|
||||||
|
"name": "funnel_chart",
|
||||||
|
"title": "销售漏斗图",
|
||||||
|
"chart_type": "funnel",
|
||||||
|
"data_source": "funnel_analysis_api",
|
||||||
|
"config": {
|
||||||
|
"value_field": "total_amount",
|
||||||
|
"category_field": "sales_stage",
|
||||||
|
"show_percentage": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chart",
|
||||||
|
"name": "amount_pie",
|
||||||
|
"title": "各阶段金额占比",
|
||||||
|
"chart_type": "pie",
|
||||||
|
"data_source": "funnel_analysis_api",
|
||||||
|
"config": {
|
||||||
|
"value_field": "total_amount",
|
||||||
|
"category_field": "sales_stage"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"name": "summary_stats",
|
||||||
|
"title": "汇总统计",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "statistic",
|
||||||
|
"name": "total_opportunities",
|
||||||
|
"label": "商机总数"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "statistic",
|
||||||
|
"name": "total_amount",
|
||||||
|
"label": "预估总金额"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "statistic",
|
||||||
|
"name": "predicted_revenue",
|
||||||
|
"label": "预测成交金额"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
37
json/opportunities_edit.json
Normal file
37
json/opportunities_edit.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"tblname": "opportunities",
|
||||||
|
"alias": "opportunities_edit",
|
||||||
|
"title": "编辑商机",
|
||||||
|
"params": {
|
||||||
|
"formfields": {
|
||||||
|
"exclouded": ["id", "customer_id", "owner_id", "org_id", "created_at", "updated_at"],
|
||||||
|
"alters": {
|
||||||
|
"sales_stage": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "初步接洽", "text": "初步接洽"},
|
||||||
|
{"value": "需求确认", "text": "需求确认"},
|
||||||
|
{"value": "方案报价", "text": "方案报价"},
|
||||||
|
{"value": "合同谈判", "text": "合同谈判"},
|
||||||
|
{"value": "成交", "text": "成交"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "active", "text": "活跃"},
|
||||||
|
{"value": "won", "text": "已成交"},
|
||||||
|
{"value": "lost", "text": "已丢失"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "manual", "text": "手动录入"},
|
||||||
|
{"value": "lead_conversion", "text": "线索转化"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
json/opportunities_list.json
Normal file
38
json/opportunities_list.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"tblname": "opportunities",
|
||||||
|
"alias": "opportunities_list",
|
||||||
|
"title": "商机列表",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["created_at desc"],
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["id", "customer_id", "owner_id", "org_id"],
|
||||||
|
"alters": {
|
||||||
|
"sales_stage": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "初步接洽", "text": "初步接洽"},
|
||||||
|
{"value": "需求确认", "text": "需求确认"},
|
||||||
|
{"value": "方案报价", "text": "方案报价"},
|
||||||
|
{"value": "合同谈判", "text": "合同谈判"},
|
||||||
|
{"value": "成交", "text": "成交"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "active", "text": "活跃"},
|
||||||
|
{"value": "won", "text": "已成交"},
|
||||||
|
{"value": "lost", "text": "已丢失"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "manual", "text": "手动录入"},
|
||||||
|
{"value": "lead_conversion", "text": "线索转化"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
json/predictions_list.json
Normal file
11
json/predictions_list.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"tblname": "opportunity_predictions",
|
||||||
|
"alias": "predictions_list",
|
||||||
|
"title": "商机预测记录",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["prediction_date desc"],
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["id", "opportunity_id"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
json/revenue_prediction.json
Normal file
103
json/revenue_prediction.json
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"name": "revenue_prediction",
|
||||||
|
"title": "收入预测",
|
||||||
|
"type": "page",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "container",
|
||||||
|
"name": "filters",
|
||||||
|
"title": "预测条件",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"name": "owner_id",
|
||||||
|
"label": "销售负责人",
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"name": "region",
|
||||||
|
"label": "区域",
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"name": "period",
|
||||||
|
"label": "历史数据周期",
|
||||||
|
"options": [
|
||||||
|
{"value": "last_3_months", "text": "最近3个月"},
|
||||||
|
{"value": "last_6_months", "text": "最近6个月"},
|
||||||
|
{"value": "last_year", "text": "最近1年"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button",
|
||||||
|
"name": "predict",
|
||||||
|
"label": "生成预测",
|
||||||
|
"action": "run_prediction"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"name": "prediction_results",
|
||||||
|
"title": "预测结果",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "statistic",
|
||||||
|
"name": "final_prediction",
|
||||||
|
"label": "最终预测收入",
|
||||||
|
"format": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "statistic",
|
||||||
|
"name": "stage_based_prediction",
|
||||||
|
"label": "阶段概率预测",
|
||||||
|
"format": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "statistic",
|
||||||
|
"name": "historical_based_prediction",
|
||||||
|
"label": "历史转化率预测",
|
||||||
|
"format": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "progress",
|
||||||
|
"name": "confidence_level",
|
||||||
|
"label": "预测置信度",
|
||||||
|
"value_field": "confidence_level",
|
||||||
|
"config": {
|
||||||
|
"high": 90,
|
||||||
|
"medium": 70,
|
||||||
|
"low": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chart",
|
||||||
|
"name": "historical_conversion",
|
||||||
|
"title": "历史转化率趋势",
|
||||||
|
"chart_type": "line",
|
||||||
|
"data_source": "conversion_rate_analysis_api",
|
||||||
|
"config": {
|
||||||
|
"x_field": "month",
|
||||||
|
"y_field": "conversion_rate",
|
||||||
|
"show_markers": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"name": "opportunity_details",
|
||||||
|
"title": "商机明细",
|
||||||
|
"data_source": "active_opportunities_api",
|
||||||
|
"columns": [
|
||||||
|
{"field": "customer_name", "title": "客户名称"},
|
||||||
|
{"field": "estimated_amount", "title": "预估金额", "format": "currency"},
|
||||||
|
{"field": "sales_stage", "title": "销售阶段"},
|
||||||
|
{"field": "probability", "title": "成交概率", "format": "percentage"},
|
||||||
|
{"field": "expected_close_date", "title": "预计成交时间"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
26
json/sales_stages_list.json
Normal file
26
json/sales_stages_list.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"tblname": "sales_stages",
|
||||||
|
"alias": "sales_stages_list",
|
||||||
|
"title": "销售阶段管理",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["stage_order"],
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["id", "updated_at"],
|
||||||
|
"alters": {
|
||||||
|
"is_active": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"value": "1",
|
||||||
|
"text": "启用"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "0",
|
||||||
|
"text": "禁用"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
json/stage_change.json
Normal file
44
json/stage_change.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "stage_change",
|
||||||
|
"title": "商机阶段变更",
|
||||||
|
"type": "form",
|
||||||
|
"data_source": "opportunity_by_id_api",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "display",
|
||||||
|
"name": "customer_name",
|
||||||
|
"label": "客户名称"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "display",
|
||||||
|
"name": "current_stage",
|
||||||
|
"label": "当前阶段"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"name": "new_stage",
|
||||||
|
"label": "新阶段",
|
||||||
|
"required": true,
|
||||||
|
"options": [
|
||||||
|
{"value": "初步接洽", "text": "初步接洽"},
|
||||||
|
{"value": "需求确认", "text": "需求确认"},
|
||||||
|
{"value": "方案报价", "text": "方案报价"},
|
||||||
|
{"value": "合同谈判", "text": "合同谈判"},
|
||||||
|
{"value": "成交", "text": "成交"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "textarea",
|
||||||
|
"name": "change_reason",
|
||||||
|
"label": "变更原因",
|
||||||
|
"required": true,
|
||||||
|
"placeholder": "请输入阶段变更的原因..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button",
|
||||||
|
"name": "submit",
|
||||||
|
"label": "确认变更",
|
||||||
|
"action": "update_opportunity_stage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
json/stage_history_list.json
Normal file
11
json/stage_history_list.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"tblname": "opportunity_stage_history",
|
||||||
|
"alias": "stage_history_list",
|
||||||
|
"title": "阶段变更历史",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["changed_at desc"],
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["id", "opportunity_id", "changed_by"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
models/opportunities.json
Normal file
128
models/opportunities.json
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "opportunities",
|
||||||
|
"title": "商机表",
|
||||||
|
"primary": "id",
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "商机ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "主键 - UUID格式"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "customer_name",
|
||||||
|
"title": "客户名称",
|
||||||
|
"type": "str",
|
||||||
|
"length": 255,
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "客户公司或个人名称"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "estimated_amount",
|
||||||
|
"title": "预估金额",
|
||||||
|
"type": "decimal",
|
||||||
|
"length": 15,
|
||||||
|
"dec": 2,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "0.00",
|
||||||
|
"comments": "预估成交金额"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "current_stage",
|
||||||
|
"title": "当前销售阶段",
|
||||||
|
"type": "str",
|
||||||
|
"length": 50,
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "当前所处的销售阶段"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expected_close_date",
|
||||||
|
"title": "预计成交时间",
|
||||||
|
"type": "date",
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "预计成交日期"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source_type",
|
||||||
|
"title": "来源类型",
|
||||||
|
"type": "str",
|
||||||
|
"length": 20,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "manual",
|
||||||
|
"comments": "商机来源:manual=手动录入, lead=线索转化"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "owner_id",
|
||||||
|
"title": "负责人ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "负责该商机的销售人员ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "region",
|
||||||
|
"title": "区域",
|
||||||
|
"type": "str",
|
||||||
|
"length": 100,
|
||||||
|
"nullable": "yes",
|
||||||
|
"comments": "客户所在区域"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"title": "创建时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "商机创建时间"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at",
|
||||||
|
"title": "更新时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "最后更新时间"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"title": "状态",
|
||||||
|
"type": "str",
|
||||||
|
"length": 20,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "active",
|
||||||
|
"comments": "商机状态:active=活跃, won=成交, lost=丢单"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "idx_opportunities_customer",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["customer_name"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_opportunities_owner",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["owner_id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_opportunities_stage",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["current_stage"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_opportunities_region",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["region"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_opportunities_status",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["status"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
92
models/opportunity_predictions.json
Normal file
92
models/opportunity_predictions.json
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "opportunity_predictions",
|
||||||
|
"title": "商机预测表",
|
||||||
|
"primary": "id",
|
||||||
|
"catelog": "indication"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "预测记录ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "主键 - UUID格式"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "opportunity_id",
|
||||||
|
"title": "商机ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "关联的商机ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "predicted_amount",
|
||||||
|
"title": "预测金额",
|
||||||
|
"type": "decimal",
|
||||||
|
"length": 15,
|
||||||
|
"dec": 2,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "0.00",
|
||||||
|
"comments": "基于历史转化率计算的预测成交金额"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "confidence_level",
|
||||||
|
"title": "置信度",
|
||||||
|
"type": "decimal",
|
||||||
|
"length": 5,
|
||||||
|
"dec": 4,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "0.0000",
|
||||||
|
"comments": "预测的置信度(0-1)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "prediction_date",
|
||||||
|
"title": "预测日期",
|
||||||
|
"type": "date",
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "预测生成日期"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "actual_amount",
|
||||||
|
"title": "实际金额",
|
||||||
|
"type": "decimal",
|
||||||
|
"length": 15,
|
||||||
|
"dec": 2,
|
||||||
|
"nullable": "yes",
|
||||||
|
"comments": "实际成交金额(成交后更新)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deviation_rate",
|
||||||
|
"title": "偏差率",
|
||||||
|
"type": "decimal",
|
||||||
|
"length": 5,
|
||||||
|
"dec": 4,
|
||||||
|
"nullable": "yes",
|
||||||
|
"comments": "预测与实际的偏差率"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"title": "创建时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "记录创建时间"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "idx_predictions_opportunity",
|
||||||
|
"idxtype": "unique",
|
||||||
|
"idxfields": ["opportunity_id", "prediction_date"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_predictions_date",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["prediction_date"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
78
models/opportunity_stage_history.json
Normal file
78
models/opportunity_stage_history.json
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "opportunity_stage_history",
|
||||||
|
"title": "商机阶段变更历史表",
|
||||||
|
"primary": "id",
|
||||||
|
"catelog": "relation"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "历史记录ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "主键 - UUID格式"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "opportunity_id",
|
||||||
|
"title": "商机ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "关联的商机ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "from_stage",
|
||||||
|
"title": "原阶段",
|
||||||
|
"type": "str",
|
||||||
|
"length": 50,
|
||||||
|
"nullable": "yes",
|
||||||
|
"comments": "变更前的阶段"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "to_stage",
|
||||||
|
"title": "目标阶段",
|
||||||
|
"type": "str",
|
||||||
|
"length": 50,
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "变更后的阶段"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "change_reason",
|
||||||
|
"title": "变更原因",
|
||||||
|
"type": "text",
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "阶段变更的原因说明"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "changed_by",
|
||||||
|
"title": "变更人ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "执行变更的用户ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "changed_at",
|
||||||
|
"title": "变更时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "变更时间"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "idx_stage_history_opportunity",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["opportunity_id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_stage_history_changed_by",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["changed_by"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
87
models/sales_stages.json
Normal file
87
models/sales_stages.json
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "sales_stages",
|
||||||
|
"title": "销售阶段表",
|
||||||
|
"primary": "id",
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "阶段ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "主键 - UUID格式"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stage_name",
|
||||||
|
"title": "阶段名称",
|
||||||
|
"type": "str",
|
||||||
|
"length": 100,
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "销售阶段名称"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stage_order",
|
||||||
|
"title": "阶段顺序",
|
||||||
|
"type": "long",
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "阶段在销售漏斗中的顺序"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"title": "阶段描述",
|
||||||
|
"type": "text",
|
||||||
|
"nullable": "yes",
|
||||||
|
"comments": "阶段详细描述"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "conversion_rate",
|
||||||
|
"title": "历史转化率",
|
||||||
|
"type": "decimal",
|
||||||
|
"length": 5,
|
||||||
|
"dec": 4,
|
||||||
|
"nullable": "yes",
|
||||||
|
"default": "0.0000",
|
||||||
|
"comments": "该阶段到下一阶段的历史平均转化率"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"title": "创建时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "创建时间"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at",
|
||||||
|
"title": "更新时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no",
|
||||||
|
"comments": "最后更新时间"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "is_active",
|
||||||
|
"title": "是否启用",
|
||||||
|
"type": "str",
|
||||||
|
"length": 1,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "1",
|
||||||
|
"comments": "是否启用:1=启用, 0=禁用"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "idx_sales_stages_order",
|
||||||
|
"idxtype": "unique",
|
||||||
|
"idxfields": ["stage_order"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_sales_stages_name",
|
||||||
|
"idxtype": "unique",
|
||||||
|
"idxfields": ["stage_name"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
32
opportunity_management/__init__.py
Normal file
32
opportunity_management/__init__.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""
|
||||||
|
商机管理模块
|
||||||
|
实现商机全生命周期管理和商机分析功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .opportunity_core import (
|
||||||
|
create_opportunity,
|
||||||
|
update_opportunity_stage,
|
||||||
|
get_funnel_analysis,
|
||||||
|
predict_revenue,
|
||||||
|
update_opportunity,
|
||||||
|
delete_opportunity,
|
||||||
|
get_opportunity_by_id,
|
||||||
|
list_opportunities
|
||||||
|
)
|
||||||
|
|
||||||
|
# 版本信息
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__author__ = "Hermes AI Agent"
|
||||||
|
__description__ = "Opportunity Management Module with Full Lifecycle and Analytics"
|
||||||
|
|
||||||
|
# 导出所有公共接口
|
||||||
|
__all__ = [
|
||||||
|
'create_opportunity',
|
||||||
|
'update_opportunity_stage',
|
||||||
|
'get_funnel_analysis',
|
||||||
|
'predict_revenue',
|
||||||
|
'update_opportunity',
|
||||||
|
'delete_opportunity',
|
||||||
|
'get_opportunity_by_id',
|
||||||
|
'list_opportunities'
|
||||||
|
]
|
||||||
BIN
opportunity_management/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
opportunity_management/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
271
opportunity_management/core.py
Normal file
271
opportunity_management/core.py
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
from datetime import datetime, date
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
from appPublic.worker import awaitify
|
||||||
|
from sqlor.dbp import DBP
|
||||||
|
|
||||||
|
|
||||||
|
async def create_opportunity(
|
||||||
|
customer_name: str,
|
||||||
|
estimated_amount: float,
|
||||||
|
current_stage: str,
|
||||||
|
expected_close_date: str,
|
||||||
|
source_type: str = "manual",
|
||||||
|
owner_id: str = None,
|
||||||
|
region: str = None
|
||||||
|
) -> Dict:
|
||||||
|
"""创建商机"""
|
||||||
|
dbp = DBP()
|
||||||
|
opportunity_id = str(uuid.uuid4()).replace('-', '')
|
||||||
|
|
||||||
|
# 验证阶段是否存在
|
||||||
|
stage_exists = await dbp.select_one(
|
||||||
|
"sales_stages",
|
||||||
|
{"stage_name": current_stage, "is_active": "1"}
|
||||||
|
)
|
||||||
|
if not stage_exists:
|
||||||
|
raise ValueError(f"销售阶段 '{current_stage}' 不存在或未启用")
|
||||||
|
|
||||||
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
opportunity_data = {
|
||||||
|
"id": opportunity_id,
|
||||||
|
"customer_name": customer_name,
|
||||||
|
"estimated_amount": estimated_amount,
|
||||||
|
"current_stage": current_stage,
|
||||||
|
"expected_close_date": expected_close_date,
|
||||||
|
"source_type": source_type,
|
||||||
|
"owner_id": owner_id or get_current_user_id(),
|
||||||
|
"region": region,
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
"status": "active"
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbp.insert("opportunities", opportunity_data)
|
||||||
|
|
||||||
|
# 创建初始预测记录
|
||||||
|
await create_prediction_record(opportunity_id, estimated_amount, current_stage)
|
||||||
|
|
||||||
|
return opportunity_data
|
||||||
|
|
||||||
|
|
||||||
|
async def update_opportunity_stage(
|
||||||
|
opportunity_id: str,
|
||||||
|
new_stage: str,
|
||||||
|
change_reason: str,
|
||||||
|
changed_by: str = None
|
||||||
|
) -> Dict:
|
||||||
|
"""更新商机阶段"""
|
||||||
|
dbp = DBP()
|
||||||
|
changed_by = changed_by or get_current_user_id()
|
||||||
|
|
||||||
|
# 获取当前商机信息
|
||||||
|
opportunity = await dbp.select_one("opportunities", {"id": opportunity_id})
|
||||||
|
if not opportunity:
|
||||||
|
raise ValueError("商机不存在")
|
||||||
|
|
||||||
|
if opportunity["current_stage"] == new_stage:
|
||||||
|
raise ValueError("商机已在目标阶段")
|
||||||
|
|
||||||
|
# 验证新阶段是否存在
|
||||||
|
new_stage_info = await dbp.select_one(
|
||||||
|
"sales_stages",
|
||||||
|
{"stage_name": new_stage, "is_active": "1"}
|
||||||
|
)
|
||||||
|
if not new_stage_info:
|
||||||
|
raise ValueError(f"销售阶段 '{new_stage}' 不存在或未启用")
|
||||||
|
|
||||||
|
# 记录阶段变更历史
|
||||||
|
history_id = str(uuid.uuid4()).replace('-', '')
|
||||||
|
history_data = {
|
||||||
|
"id": history_id,
|
||||||
|
"opportunity_id": opportunity_id,
|
||||||
|
"from_stage": opportunity["current_stage"],
|
||||||
|
"to_stage": new_stage,
|
||||||
|
"change_reason": change_reason,
|
||||||
|
"changed_by": changed_by,
|
||||||
|
"changed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
}
|
||||||
|
await dbp.insert("opportunity_stage_history", history_data)
|
||||||
|
|
||||||
|
# 更新商机阶段
|
||||||
|
await dbp.update(
|
||||||
|
"opportunities",
|
||||||
|
{"current_stage": new_stage, "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")},
|
||||||
|
{"id": opportunity_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新预测记录
|
||||||
|
await update_prediction_record(opportunity_id, opportunity["estimated_amount"], new_stage)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"opportunity_id": opportunity_id,
|
||||||
|
"from_stage": opportunity["current_stage"],
|
||||||
|
"to_stage": new_stage,
|
||||||
|
"history_id": history_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def create_prediction_record(
|
||||||
|
opportunity_id: str,
|
||||||
|
estimated_amount: float,
|
||||||
|
current_stage: str
|
||||||
|
) -> Dict:
|
||||||
|
"""创建预测记录"""
|
||||||
|
dbp = DBP()
|
||||||
|
|
||||||
|
# 获取当前阶段的转化率
|
||||||
|
stage_info = await dbp.select_one("sales_stages", {"stage_name": current_stage})
|
||||||
|
conversion_rate = float(stage_info.get("conversion_rate", "0.0000")) if stage_info else 0.0
|
||||||
|
|
||||||
|
predicted_amount = estimated_amount * conversion_rate if conversion_rate > 0 else estimated_amount
|
||||||
|
confidence_level = min(conversion_rate, 1.0) if conversion_rate > 0 else 0.5
|
||||||
|
|
||||||
|
prediction_id = str(uuid.uuid4()).replace('-', '')
|
||||||
|
prediction_data = {
|
||||||
|
"id": prediction_id,
|
||||||
|
"opportunity_id": opportunity_id,
|
||||||
|
"predicted_amount": predicted_amount,
|
||||||
|
"confidence_level": confidence_level,
|
||||||
|
"prediction_date": date.today().strftime("%Y-%m-%d"),
|
||||||
|
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbp.insert("opportunity_predictions", prediction_data)
|
||||||
|
return prediction_data
|
||||||
|
|
||||||
|
|
||||||
|
async def update_prediction_record(
|
||||||
|
opportunity_id: str,
|
||||||
|
estimated_amount: float,
|
||||||
|
current_stage: str
|
||||||
|
) -> Dict:
|
||||||
|
"""更新预测记录"""
|
||||||
|
dbp = DBP()
|
||||||
|
|
||||||
|
# 获取当前阶段的转化率
|
||||||
|
stage_info = await dbp.select_one("sales_stages", {"stage_name": current_stage})
|
||||||
|
conversion_rate = float(stage_info.get("conversion_rate", "0.0000")) if stage_info else 0.0
|
||||||
|
|
||||||
|
predicted_amount = estimated_amount * conversion_rate if conversion_rate > 0 else estimated_amount
|
||||||
|
confidence_level = min(conversion_rate, 1.0) if conversion_rate > 0 else 0.5
|
||||||
|
|
||||||
|
# 检查今天是否已有预测记录
|
||||||
|
today = date.today().strftime("%Y-%m-%d")
|
||||||
|
existing_prediction = await dbp.select_one(
|
||||||
|
"opportunity_predictions",
|
||||||
|
{"opportunity_id": opportunity_id, "prediction_date": today}
|
||||||
|
)
|
||||||
|
|
||||||
|
prediction_data = {
|
||||||
|
"predicted_amount": predicted_amount,
|
||||||
|
"confidence_level": confidence_level,
|
||||||
|
"prediction_date": today,
|
||||||
|
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing_prediction:
|
||||||
|
# 更新现有记录
|
||||||
|
await dbp.update(
|
||||||
|
"opportunity_predictions",
|
||||||
|
prediction_data,
|
||||||
|
{"id": existing_prediction["id"]}
|
||||||
|
)
|
||||||
|
prediction_data["id"] = existing_prediction["id"]
|
||||||
|
else:
|
||||||
|
# 创建新记录
|
||||||
|
prediction_id = str(uuid.uuid4()).replace('-', '')
|
||||||
|
prediction_data["id"] = prediction_id
|
||||||
|
prediction_data["opportunity_id"] = opportunity_id
|
||||||
|
await dbp.insert("opportunity_predictions", prediction_data)
|
||||||
|
|
||||||
|
return prediction_data
|
||||||
|
|
||||||
|
|
||||||
|
async def get_funnel_analysis(
|
||||||
|
region: str = None,
|
||||||
|
owner_id: str = None
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""获取销售漏斗分析数据"""
|
||||||
|
dbp = DBP()
|
||||||
|
|
||||||
|
# 构建查询条件
|
||||||
|
where_clause = {}
|
||||||
|
if region:
|
||||||
|
where_clause["region"] = region
|
||||||
|
if owner_id:
|
||||||
|
where_clause["owner_id"] = owner_id
|
||||||
|
|
||||||
|
# 获取所有活跃商机按阶段分组
|
||||||
|
funnel_data = await dbp.query("""
|
||||||
|
SELECT
|
||||||
|
o.current_stage,
|
||||||
|
s.stage_order,
|
||||||
|
COUNT(*) as opportunity_count,
|
||||||
|
SUM(o.estimated_amount) as total_amount,
|
||||||
|
AVG(p.predicted_amount) as avg_predicted_amount
|
||||||
|
FROM opportunities o
|
||||||
|
LEFT JOIN sales_stages s ON o.current_stage = s.stage_name
|
||||||
|
LEFT JOIN opportunity_predictions p ON o.id = p.opportunity_id
|
||||||
|
AND p.prediction_date = CURDATE()
|
||||||
|
WHERE o.status = 'active'
|
||||||
|
""" + (" AND o.region = %(region)s" if region else "") +
|
||||||
|
(" AND o.owner_id = %(owner_id)s" if owner_id else "") + """
|
||||||
|
GROUP BY o.current_stage, s.stage_order
|
||||||
|
ORDER BY s.stage_order ASC
|
||||||
|
""", where_clause)
|
||||||
|
|
||||||
|
return funnel_data
|
||||||
|
|
||||||
|
|
||||||
|
async def get_sales_prediction(
|
||||||
|
region: str = None,
|
||||||
|
owner_id: str = None
|
||||||
|
) -> Dict:
|
||||||
|
"""获取销售预测汇总"""
|
||||||
|
dbp = DBP()
|
||||||
|
|
||||||
|
where_clause = {"prediction_date": date.today().strftime("%Y-%m-%d")}
|
||||||
|
if region:
|
||||||
|
where_clause["region"] = region
|
||||||
|
if owner_id:
|
||||||
|
where_clause["owner_id"] = owner_id
|
||||||
|
|
||||||
|
# 获取今日预测汇总
|
||||||
|
prediction_summary = await dbp.query("""
|
||||||
|
SELECT
|
||||||
|
SUM(p.predicted_amount) as total_predicted,
|
||||||
|
SUM(p.confidence_level) / COUNT(*) as avg_confidence,
|
||||||
|
COUNT(*) as opportunity_count
|
||||||
|
FROM opportunity_predictions p
|
||||||
|
JOIN opportunities o ON p.opportunity_id = o.id
|
||||||
|
WHERE p.prediction_date = %(prediction_date)s
|
||||||
|
""" + (" AND o.region = %(region)s" if region else "") +
|
||||||
|
(" AND o.owner_id = %(owner_id)s" if owner_id else ""), where_clause)
|
||||||
|
|
||||||
|
if prediction_summary:
|
||||||
|
return prediction_summary[0]
|
||||||
|
return {"total_predicted": 0, "avg_confidence": 0, "opportunity_count": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_user_id() -> str:
|
||||||
|
"""获取当前用户ID(模拟实现)"""
|
||||||
|
# 在实际实现中,这里应该从会话或认证信息中获取
|
||||||
|
return "current_user_id"
|
||||||
|
|
||||||
|
|
||||||
|
# 同步版本函数(用于前端调用)
|
||||||
|
def sync_create_opportunity(*args, **kwargs):
|
||||||
|
return create_opportunity(*args, **kwargs)
|
||||||
|
|
||||||
|
def sync_update_opportunity_stage(*args, **kwargs):
|
||||||
|
return update_opportunity_stage(*args, **kwargs)
|
||||||
|
|
||||||
|
def sync_get_funnel_analysis(*args, **kwargs):
|
||||||
|
return get_funnel_analysis(*args, **kwargs)
|
||||||
|
|
||||||
|
def sync_get_sales_prediction(*args, **kwargs):
|
||||||
|
return get_sales_prediction(*args, **kwargs)
|
||||||
16
opportunity_management/init.py
Normal file
16
opportunity_management/init.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
from appPublic.worker import awaitify
|
||||||
|
from .core import (
|
||||||
|
sync_create_opportunity,
|
||||||
|
sync_update_opportunity_stage,
|
||||||
|
sync_get_funnel_analysis,
|
||||||
|
sync_get_sales_prediction
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_opportunity_management():
|
||||||
|
env = ServerEnv()
|
||||||
|
env.create_opportunity = awaitify(sync_create_opportunity)
|
||||||
|
env.update_opportunity_stage = awaitify(sync_update_opportunity_stage)
|
||||||
|
env.get_funnel_analysis = awaitify(sync_get_funnel_analysis)
|
||||||
|
env.get_sales_prediction = awaitify(sync_get_sales_prediction)
|
||||||
399
opportunity_management/opportunity_core.py
Normal file
399
opportunity_management/opportunity_core.py
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
"""
|
||||||
|
商机管理模块 - 核心业务逻辑
|
||||||
|
实现商机全生命周期管理和商机分析功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, date
|
||||||
|
from typing import List, Dict, Optional, Tuple
|
||||||
|
from appPublic.jsonconfig import getConfig
|
||||||
|
from appPublic.worker import Worker
|
||||||
|
from sqlor.dbp import getDBP
|
||||||
|
|
||||||
|
|
||||||
|
class OpportunityManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.config = getConfig()
|
||||||
|
self.worker = Worker()
|
||||||
|
|
||||||
|
async def get_db_connection(self, org_id: str):
|
||||||
|
"""获取数据库连接"""
|
||||||
|
dbp = await getDBP(org_id)
|
||||||
|
return dbp
|
||||||
|
|
||||||
|
async def create_opportunity(self, opportunity_data: Dict, user_id: str, org_id: str) -> str:
|
||||||
|
"""创建商机"""
|
||||||
|
# 验证必填字段
|
||||||
|
required_fields = ['customer_name', 'estimated_amount', 'sales_stage', 'expected_close_date']
|
||||||
|
for field in required_fields:
|
||||||
|
if not opportunity_data.get(field):
|
||||||
|
raise ValueError(f"缺少必填字段: {field}")
|
||||||
|
|
||||||
|
opportunity_id = str(uuid.uuid4()).replace('-', '')
|
||||||
|
dbp = await self.get_db_connection(org_id)
|
||||||
|
|
||||||
|
# 插入商机数据
|
||||||
|
sql = """
|
||||||
|
INSERT INTO opportunities (
|
||||||
|
id, customer_name, customer_id, estimated_amount, sales_stage,
|
||||||
|
expected_close_date, source, description, owner_id, org_id,
|
||||||
|
status, created_at, updated_at, probability, next_action_date,
|
||||||
|
next_action_description, tags
|
||||||
|
) VALUES (
|
||||||
|
%(id)s, %(customer_name)s, %(customer_id)s, %(estimated_amount)s, %(sales_stage)s,
|
||||||
|
%(expected_close_date)s, %(source)s, %(description)s, %(owner_id)s, %(org_id)s,
|
||||||
|
%(status)s, NOW(), NOW(), %(probability)s, %(next_action_date)s,
|
||||||
|
%(next_action_description)s, %(tags)s
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'id': opportunity_id,
|
||||||
|
'customer_name': opportunity_data['customer_name'],
|
||||||
|
'customer_id': opportunity_data.get('customer_id'),
|
||||||
|
'estimated_amount': opportunity_data['estimated_amount'],
|
||||||
|
'sales_stage': opportunity_data['sales_stage'],
|
||||||
|
'expected_close_date': opportunity_data['expected_close_date'],
|
||||||
|
'source': opportunity_data.get('source', 'manual'), # manual 或 lead_conversion
|
||||||
|
'description': opportunity_data.get('description'),
|
||||||
|
'owner_id': user_id,
|
||||||
|
'org_id': org_id,
|
||||||
|
'status': opportunity_data.get('status', 'active'),
|
||||||
|
'probability': opportunity_data.get('probability', self._calculate_probability(opportunity_data['sales_stage'])),
|
||||||
|
'next_action_date': opportunity_data.get('next_action_date'),
|
||||||
|
'next_action_description': opportunity_data.get('next_action_description'),
|
||||||
|
'tags': opportunity_data.get('tags')
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbp.doTransaction([{'sql': sql, 'params': params}])
|
||||||
|
return opportunity_id
|
||||||
|
|
||||||
|
def _calculate_probability(self, sales_stage: str) -> float:
|
||||||
|
"""根据销售阶段计算成交概率"""
|
||||||
|
stage_probabilities = {
|
||||||
|
'初步接洽': 0.1,
|
||||||
|
'需求确认': 0.3,
|
||||||
|
'方案报价': 0.5,
|
||||||
|
'合同谈判': 0.7,
|
||||||
|
'成交': 1.0
|
||||||
|
}
|
||||||
|
return stage_probabilities.get(sales_stage, 0.1)
|
||||||
|
|
||||||
|
async def update_opportunity_stage(self, opportunity_id: str, new_stage: str,
|
||||||
|
change_reason: str, user_id: str, org_id: str) -> bool:
|
||||||
|
"""更新商机阶段"""
|
||||||
|
dbp = await self.get_db_connection(org_id)
|
||||||
|
|
||||||
|
# 获取当前商机信息
|
||||||
|
current_opportunity = await dbp.select_one("opportunities", {"id": opportunity_id, "org_id": org_id})
|
||||||
|
if not current_opportunity:
|
||||||
|
raise ValueError("商机不存在")
|
||||||
|
|
||||||
|
old_stage = current_opportunity['sales_stage']
|
||||||
|
|
||||||
|
# 更新商机阶段和概率
|
||||||
|
new_probability = self._calculate_probability(new_stage)
|
||||||
|
update_data = {
|
||||||
|
'sales_stage': new_stage,
|
||||||
|
'probability': new_probability,
|
||||||
|
'updated_at': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
}
|
||||||
|
|
||||||
|
# 如果阶段变为成交,更新状态
|
||||||
|
if new_stage == '成交':
|
||||||
|
update_data['status'] = 'won'
|
||||||
|
update_data['actual_close_date'] = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# 如果阶段回退,可能需要更新状态
|
||||||
|
elif old_stage == '成交' and new_stage != '成交':
|
||||||
|
update_data['status'] = 'active'
|
||||||
|
update_data['actual_close_date'] = None
|
||||||
|
|
||||||
|
result = await dbp.update("opportunities", update_data, {"id": opportunity_id, "org_id": org_id})
|
||||||
|
|
||||||
|
# 记录阶段变更历史
|
||||||
|
if old_stage != new_stage:
|
||||||
|
await self._record_stage_change(opportunity_id, old_stage, new_stage, change_reason, user_id, org_id)
|
||||||
|
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
|
async def _record_stage_change(self, opportunity_id: str, old_stage: str,
|
||||||
|
new_stage: str, change_reason: str, user_id: str, org_id: str):
|
||||||
|
"""记录阶段变更历史"""
|
||||||
|
history_id = str(uuid.uuid4()).replace('-', '')
|
||||||
|
dbp = await self.get_db_connection(org_id)
|
||||||
|
|
||||||
|
history_data = {
|
||||||
|
'id': history_id,
|
||||||
|
'opportunity_id': opportunity_id,
|
||||||
|
'old_stage': old_stage,
|
||||||
|
'new_stage': new_stage,
|
||||||
|
'change_reason': change_reason,
|
||||||
|
'changed_by': user_id,
|
||||||
|
'created_at': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbp.insert("opportunity_stage_history", history_data)
|
||||||
|
|
||||||
|
async def get_funnel_analysis(self, org_id: str, filters: Optional[Dict] = None) -> Dict:
|
||||||
|
"""获取销售漏斗分析数据"""
|
||||||
|
dbp = await self.get_db_connection(org_id)
|
||||||
|
|
||||||
|
# 构建查询条件
|
||||||
|
where_clauses = ["o.org_id = %(org_id)s", "o.status = 'active'"]
|
||||||
|
params = {'org_id': org_id}
|
||||||
|
|
||||||
|
if filters:
|
||||||
|
if filters.get('region'):
|
||||||
|
where_clauses.append("c.region = %(region)s")
|
||||||
|
params['region'] = filters['region']
|
||||||
|
if filters.get('owner_id'):
|
||||||
|
where_clauses.append("o.owner_id = %(owner_id)s")
|
||||||
|
params['owner_id'] = filters['owner_id']
|
||||||
|
if filters.get('date_range'):
|
||||||
|
start_date, end_date = filters['date_range']
|
||||||
|
where_clauses.append("o.created_at BETWEEN %(start_date)s AND %(end_date)s")
|
||||||
|
params['start_date'] = start_date
|
||||||
|
params['end_date'] = end_date
|
||||||
|
|
||||||
|
where_sql = " AND ".join(where_clauses)
|
||||||
|
|
||||||
|
# 获取各阶段商机数量和金额
|
||||||
|
funnel_sql = f"""
|
||||||
|
SELECT
|
||||||
|
o.sales_stage,
|
||||||
|
COUNT(*) as opportunity_count,
|
||||||
|
SUM(o.estimated_amount) as total_amount,
|
||||||
|
AVG(o.probability) as avg_probability
|
||||||
|
FROM opportunities o
|
||||||
|
LEFT JOIN customers c ON o.customer_id = c.id
|
||||||
|
WHERE {where_sql}
|
||||||
|
GROUP BY o.sales_stage
|
||||||
|
ORDER BY
|
||||||
|
CASE o.sales_stage
|
||||||
|
WHEN '初步接洽' THEN 1
|
||||||
|
WHEN '需求确认' THEN 2
|
||||||
|
WHEN '方案报价' THEN 3
|
||||||
|
WHEN '合同谈判' THEN 4
|
||||||
|
WHEN '成交' THEN 5
|
||||||
|
ELSE 99
|
||||||
|
END
|
||||||
|
"""
|
||||||
|
|
||||||
|
funnel_data = await dbp.doQuery(funnel_sql, params)
|
||||||
|
|
||||||
|
# 计算预测成交金额
|
||||||
|
predicted_revenue = 0
|
||||||
|
for item in funnel_data:
|
||||||
|
predicted_revenue += float(item['total_amount']) * float(item['avg_probability'])
|
||||||
|
|
||||||
|
return {
|
||||||
|
'funnel_data': funnel_data,
|
||||||
|
'predicted_revenue': predicted_revenue,
|
||||||
|
'total_opportunities': sum(item['opportunity_count'] for item in funnel_data),
|
||||||
|
'total_estimated_amount': sum(float(item['total_amount']) for item in funnel_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_conversion_rate_analysis(self, org_id: str, period: str = 'last_6_months') -> Dict:
|
||||||
|
"""获取转化率分析数据(用于预测功能)"""
|
||||||
|
dbp = await self.get_db_connection(org_id)
|
||||||
|
|
||||||
|
# 确定时间范围
|
||||||
|
if period == 'last_6_months':
|
||||||
|
start_date = (date.today().replace(day=1) - timedelta(days=180)).strftime("%Y-%m-01")
|
||||||
|
elif period == 'last_year':
|
||||||
|
start_date = (date.today().replace(day=1) - timedelta(days=365)).strftime("%Y-%m-01")
|
||||||
|
else:
|
||||||
|
start_date = (date.today() - timedelta(days=90)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# 获取历史转化数据
|
||||||
|
conversion_sql = """
|
||||||
|
SELECT
|
||||||
|
DATE_FORMAT(created_at, '%%Y-%%m') as month,
|
||||||
|
COUNT(*) as total_opportunities,
|
||||||
|
SUM(CASE WHEN status = 'won' THEN 1 ELSE 0 END) as won_opportunities,
|
||||||
|
AVG(CASE WHEN status = 'won' THEN estimated_amount ELSE 0 END) as avg_won_amount
|
||||||
|
FROM opportunities
|
||||||
|
WHERE org_id = %(org_id)s
|
||||||
|
AND created_at >= %(start_date)s
|
||||||
|
GROUP BY DATE_FORMAT(created_at, '%%Y-%%m')
|
||||||
|
ORDER BY month
|
||||||
|
"""
|
||||||
|
|
||||||
|
conversion_data = await dbp.doQuery(conversion_sql, {'org_id': org_id, 'start_date': start_date})
|
||||||
|
|
||||||
|
# 计算整体转化率
|
||||||
|
total_opps = sum(item['total_opportunities'] for item in conversion_data)
|
||||||
|
total_won = sum(item['won_opportunities'] for item in conversion_data)
|
||||||
|
overall_conversion_rate = total_won / total_opps if total_opps > 0 else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'conversion_data': conversion_data,
|
||||||
|
'overall_conversion_rate': overall_conversion_rate,
|
||||||
|
'period': period,
|
||||||
|
'start_date': start_date
|
||||||
|
}
|
||||||
|
|
||||||
|
async def predict_revenue(self, org_id: str, filters: Optional[Dict] = None) -> Dict:
|
||||||
|
"""预测收入(基于历史转化率)"""
|
||||||
|
# 获取当前活跃商机
|
||||||
|
dbp = await self.get_db_connection(org_id)
|
||||||
|
|
||||||
|
where_clauses = ["org_id = %(org_id)s", "status = 'active'"]
|
||||||
|
params = {'org_id': org_id}
|
||||||
|
|
||||||
|
if filters:
|
||||||
|
if filters.get('owner_id'):
|
||||||
|
where_clauses.append("owner_id = %(owner_id)s")
|
||||||
|
params['owner_id'] = filters['owner_id']
|
||||||
|
if filters.get('region'):
|
||||||
|
# 需要关联客户表
|
||||||
|
pass
|
||||||
|
|
||||||
|
where_sql = " AND ".join(where_clauses)
|
||||||
|
|
||||||
|
opportunities_sql = f"""
|
||||||
|
SELECT id, estimated_amount, probability, sales_stage
|
||||||
|
FROM opportunities
|
||||||
|
WHERE {where_sql}
|
||||||
|
"""
|
||||||
|
|
||||||
|
current_opportunities = await dbp.doQuery(opportunities_sql, params)
|
||||||
|
|
||||||
|
# 获取历史转化率
|
||||||
|
conversion_analysis = await self.get_conversion_rate_analysis(org_id)
|
||||||
|
historical_conversion_rate = conversion_analysis['overall_conversion_rate']
|
||||||
|
|
||||||
|
# 计算预测收入
|
||||||
|
stage_based_prediction = sum(
|
||||||
|
float(opp['estimated_amount']) * float(opp['probability'])
|
||||||
|
for opp in current_opportunities
|
||||||
|
)
|
||||||
|
|
||||||
|
historical_based_prediction = sum(
|
||||||
|
float(opp['estimated_amount']) * historical_conversion_rate
|
||||||
|
for opp in current_opportunities
|
||||||
|
)
|
||||||
|
|
||||||
|
# 使用加权平均(70%阶段概率 + 30%历史转化率)
|
||||||
|
final_prediction = stage_based_prediction * 0.7 + historical_based_prediction * 0.3
|
||||||
|
|
||||||
|
return {
|
||||||
|
'stage_based_prediction': stage_based_prediction,
|
||||||
|
'historical_based_prediction': historical_based_prediction,
|
||||||
|
'final_prediction': final_prediction,
|
||||||
|
'opportunity_count': len(current_opportunities),
|
||||||
|
'total_estimated_amount': sum(float(opp['estimated_amount']) for opp in current_opportunities),
|
||||||
|
'confidence_level': 'high' if len(current_opportunities) > 10 else 'medium'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 基础CRUD操作
|
||||||
|
async def update_opportunity(self, opportunity_id: str, opportunity_data: Dict, org_id: str) -> bool:
|
||||||
|
"""更新商机"""
|
||||||
|
dbp = await self.get_db_connection(org_id)
|
||||||
|
opportunity_data['updated_at'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
result = await dbp.update("opportunities", opportunity_data, {"id": opportunity_id, "org_id": org_id})
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
|
async def delete_opportunity(self, opportunity_id: str, org_id: str) -> bool:
|
||||||
|
"""删除商机(软删除)"""
|
||||||
|
dbp = await self.get_db_connection(org_id)
|
||||||
|
update_data = {
|
||||||
|
'status': 'deleted',
|
||||||
|
'updated_at': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
}
|
||||||
|
result = await dbp.update("opportunities", update_data, {"id": opportunity_id, "org_id": org_id})
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
|
async def get_opportunity_by_id(self, opportunity_id: str, org_id: str) -> Optional[Dict]:
|
||||||
|
"""根据ID获取商机"""
|
||||||
|
dbp = await self.get_db_connection(org_id)
|
||||||
|
sql = "SELECT * FROM opportunities WHERE id = %(id)s AND org_id = %(org_id)s AND status != 'deleted'"
|
||||||
|
result = await dbp.doQuery(sql, {'id': opportunity_id, 'org_id': org_id})
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
async def list_opportunities(self, org_id: str, filters: Optional[Dict] = None,
|
||||||
|
page: int = 1, page_size: int = 20) -> Tuple[List[Dict], int]:
|
||||||
|
"""列出商机"""
|
||||||
|
dbp = await self.get_db_connection(org_id)
|
||||||
|
|
||||||
|
# 构建查询条件
|
||||||
|
where_clauses = ["o.org_id = %(org_id)s", "o.status != 'deleted'"]
|
||||||
|
params = {'org_id': org_id}
|
||||||
|
|
||||||
|
if filters:
|
||||||
|
if filters.get('customer_name'):
|
||||||
|
where_clauses.append("o.customer_name LIKE %(customer_name)s")
|
||||||
|
params['customer_name'] = f"%{filters['customer_name']}%"
|
||||||
|
if filters.get('sales_stage'):
|
||||||
|
where_clauses.append("o.sales_stage = %(sales_stage)s")
|
||||||
|
params['sales_stage'] = filters['sales_stage']
|
||||||
|
if filters.get('owner_id'):
|
||||||
|
where_clauses.append("o.owner_id = %(owner_id)s")
|
||||||
|
params['owner_id'] = filters['owner_id']
|
||||||
|
if filters.get('status'):
|
||||||
|
where_clauses.append("o.status = %(status)s")
|
||||||
|
params['status'] = filters['status']
|
||||||
|
if filters.get('expected_close_date_from'):
|
||||||
|
where_clauses.append("o.expected_close_date >= %(expected_close_date_from)s")
|
||||||
|
params['expected_close_date_from'] = filters['expected_close_date_from']
|
||||||
|
if filters.get('expected_close_date_to'):
|
||||||
|
where_clauses.append("o.expected_close_date <= %(expected_close_date_to)s")
|
||||||
|
params['expected_close_date_to'] = filters['expected_close_date_to']
|
||||||
|
|
||||||
|
where_sql = " AND ".join(where_clauses)
|
||||||
|
|
||||||
|
# 获取总数
|
||||||
|
count_sql = f"SELECT COUNT(*) as total FROM opportunities o WHERE {where_sql}"
|
||||||
|
count_result = await dbp.doQuery(count_sql, params)
|
||||||
|
total = count_result[0]['total'] if count_result else 0
|
||||||
|
|
||||||
|
# 获取分页数据
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
data_sql = f"""
|
||||||
|
SELECT o.*, u.username as owner_name
|
||||||
|
FROM opportunities o
|
||||||
|
LEFT JOIN users u ON o.owner_id = u.id
|
||||||
|
WHERE {where_sql}
|
||||||
|
ORDER BY o.created_at DESC
|
||||||
|
LIMIT %(limit)s OFFSET %(offset)s
|
||||||
|
"""
|
||||||
|
params['limit'] = page_size
|
||||||
|
params['offset'] = offset
|
||||||
|
|
||||||
|
data_result = await dbp.doQuery(data_sql, params)
|
||||||
|
return data_result, total
|
||||||
|
|
||||||
|
|
||||||
|
# 全局实例
|
||||||
|
opportunity_manager = OpportunityManager()
|
||||||
|
|
||||||
|
# 导出函数
|
||||||
|
async def create_opportunity(opportunity_data: Dict, user_id: str, org_id: str) -> str:
|
||||||
|
return await opportunity_manager.create_opportunity(opportunity_data, user_id, org_id)
|
||||||
|
|
||||||
|
async def update_opportunity_stage(opportunity_id: str, new_stage: str,
|
||||||
|
change_reason: str, user_id: str, org_id: str) -> bool:
|
||||||
|
return await opportunity_manager.update_opportunity_stage(opportunity_id, new_stage, change_reason, user_id, org_id)
|
||||||
|
|
||||||
|
async def get_funnel_analysis(org_id: str, filters: Optional[Dict] = None) -> Dict:
|
||||||
|
return await opportunity_manager.get_funnel_analysis(org_id, filters)
|
||||||
|
|
||||||
|
async def predict_revenue(org_id: str, filters: Optional[Dict] = None) -> Dict:
|
||||||
|
return await opportunity_manager.predict_revenue(org_id, filters)
|
||||||
|
|
||||||
|
# 基础CRUD接口
|
||||||
|
async def update_opportunity(opportunity_id: str, opportunity_data: Dict, org_id: str) -> bool:
|
||||||
|
return await opportunity_manager.update_opportunity(opportunity_id, opportunity_data, org_id)
|
||||||
|
|
||||||
|
async def delete_opportunity(opportunity_id: str, org_id: str) -> bool:
|
||||||
|
return await opportunity_manager.delete_opportunity(opportunity_id, org_id)
|
||||||
|
|
||||||
|
async def get_opportunity_by_id(opportunity_id: str, org_id: str) -> Optional[Dict]:
|
||||||
|
return await opportunity_manager.get_opportunity_by_id(opportunity_id, org_id)
|
||||||
|
|
||||||
|
async def list_opportunities(org_id: str, filters: Optional[Dict] = None,
|
||||||
|
page: int = 1, page_size: int = 20) -> Tuple[List[Dict], int]:
|
||||||
|
return await opportunity_manager.list_opportunities(org_id, filters, page, page_size)
|
||||||
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=45", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "opportunity_management"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "商机管理模块 - 提供销售跟进和商机全生命周期管理功能"
|
||||||
|
authors = [{name = "Hermes Agent", email = "hermes@example.com"}]
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
dependencies = [
|
||||||
|
"ahserver",
|
||||||
|
"sqlor-database-module",
|
||||||
|
"bricks-framework"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["opportunity_management*"]
|
||||||
22
setup.py
Normal file
22
setup.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="opportunity_management",
|
||||||
|
version="1.0.0",
|
||||||
|
description="Opportunity Management Module with Full Lifecycle and Analytics",
|
||||||
|
author="Hermes AI Agent",
|
||||||
|
packages=find_packages(),
|
||||||
|
install_requires=[
|
||||||
|
"appPublic>=1.0.0",
|
||||||
|
"sqlor-database-module>=1.0.0"
|
||||||
|
],
|
||||||
|
python_requires=">=3.8",
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
],
|
||||||
|
)
|
||||||
67
sql/opportunity_tables.sql
Normal file
67
sql/opportunity_tables.sql
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
-- 商机管理模块数据库表结构
|
||||||
|
|
||||||
|
-- 1. 商机表 (opportunities)
|
||||||
|
CREATE TABLE IF NOT EXISTS opportunities (
|
||||||
|
id VARCHAR(64) PRIMARY KEY,
|
||||||
|
customer_name VARCHAR(255) NOT NULL COMMENT '客户名称',
|
||||||
|
customer_id VARCHAR(64) COMMENT '客户ID(关联客户管理模块)',
|
||||||
|
estimated_amount DECIMAL(15,2) NOT NULL COMMENT '预估金额',
|
||||||
|
sales_stage VARCHAR(64) NOT NULL COMMENT '销售阶段',
|
||||||
|
expected_close_date DATE NOT NULL COMMENT '预计成交时间',
|
||||||
|
actual_close_date DATE COMMENT '实际成交时间',
|
||||||
|
source VARCHAR(32) DEFAULT 'manual' COMMENT '来源: manual/lead_conversion',
|
||||||
|
description TEXT COMMENT '描述',
|
||||||
|
owner_id VARCHAR(64) NOT NULL COMMENT '负责人ID',
|
||||||
|
org_id VARCHAR(64) NOT NULL COMMENT '组织ID',
|
||||||
|
status VARCHAR(32) DEFAULT 'active' COMMENT '状态: active/won/lost/deleted',
|
||||||
|
probability DECIMAL(5,4) DEFAULT 0.1000 COMMENT '成交概率',
|
||||||
|
next_action_date DATE COMMENT '下次行动日期',
|
||||||
|
next_action_description VARCHAR(255) COMMENT '下次行动描述',
|
||||||
|
tags VARCHAR(255) COMMENT '标签',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_customer_id (customer_id),
|
||||||
|
INDEX idx_sales_stage (sales_stage),
|
||||||
|
INDEX idx_owner_id (owner_id),
|
||||||
|
INDEX idx_org_id (org_id),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_expected_close_date (expected_close_date)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 2. 商机阶段变更历史表 (opportunity_stage_history)
|
||||||
|
CREATE TABLE IF NOT EXISTS opportunity_stage_history (
|
||||||
|
id VARCHAR(64) PRIMARY KEY,
|
||||||
|
opportunity_id VARCHAR(64) NOT NULL,
|
||||||
|
old_stage VARCHAR(64) NOT NULL COMMENT '原阶段',
|
||||||
|
new_stage VARCHAR(64) NOT NULL COMMENT '新阶段',
|
||||||
|
change_reason TEXT NOT NULL COMMENT '变更原因',
|
||||||
|
changed_by VARCHAR(64) NOT NULL COMMENT '变更人ID',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_opportunity_id (opportunity_id),
|
||||||
|
INDEX idx_changed_by (changed_by),
|
||||||
|
FOREIGN KEY (opportunity_id) REFERENCES opportunities(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 3. 销售漏斗配置表 (sales_funnel_config)
|
||||||
|
CREATE TABLE IF NOT EXISTS sales_funnel_config (
|
||||||
|
id VARCHAR(64) PRIMARY KEY,
|
||||||
|
org_id VARCHAR(64) NOT NULL,
|
||||||
|
stage_name VARCHAR(64) NOT NULL COMMENT '阶段名称',
|
||||||
|
stage_order INT NOT NULL COMMENT '阶段顺序',
|
||||||
|
default_probability DECIMAL(5,4) NOT NULL COMMENT '默认成交概率',
|
||||||
|
color_code VARCHAR(16) COMMENT '颜色代码',
|
||||||
|
is_active TINYINT(1) DEFAULT 1 COMMENT '是否激活',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_org_stage (org_id, stage_name),
|
||||||
|
INDEX idx_org_id (org_id),
|
||||||
|
INDEX idx_stage_order (stage_order)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 插入默认销售漏斗配置
|
||||||
|
INSERT IGNORE INTO sales_funnel_config (id, org_id, stage_name, stage_order, default_probability, color_code) VALUES
|
||||||
|
(REPLACE(UUID(), '-', ''), 'default', '初步接洽', 1, 0.1000, '#FF6B6B'),
|
||||||
|
(REPLACE(UUID(), '-', ''), 'default', '需求确认', 2, 0.3000, '#4ECDC4'),
|
||||||
|
(REPLACE(UUID(), '-', ''), 'default', '方案报价', 3, 0.5000, '#45B7D1'),
|
||||||
|
(REPLACE(UUID(), '-', ''), 'default', '合同谈判', 4, 0.7000, '#96CEB4'),
|
||||||
|
(REPLACE(UUID(), '-', ''), 'default', '成交', 5, 1.0000, '#FFEAA7');
|
||||||
89
test_opportunity_module.py
Normal file
89
test_opportunity_module.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for Opportunity Management Module
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add the module to Python path
|
||||||
|
sys.path.insert(0, os.path.expanduser('~/repos/opportunity_management'))
|
||||||
|
|
||||||
|
from opportunity_management import create_opportunity, get_opportunity_by_id, list_opportunities
|
||||||
|
|
||||||
|
|
||||||
|
async def test_opportunity_creation():
|
||||||
|
"""Test opportunity creation"""
|
||||||
|
print("Testing opportunity creation...")
|
||||||
|
|
||||||
|
# Test data
|
||||||
|
opportunity_data = {
|
||||||
|
'customer_name': 'Test Customer',
|
||||||
|
'estimated_amount': 100000.00,
|
||||||
|
'sales_stage': '初步接洽',
|
||||||
|
'expected_close_date': '2026-12-31',
|
||||||
|
'description': 'Test opportunity created by automated test',
|
||||||
|
'source': 'manual'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create opportunity (using dummy org_id and user_id for testing)
|
||||||
|
opportunity_id = await create_opportunity(opportunity_data, 'test_user', 'test_org')
|
||||||
|
print(f"✓ Opportunity created successfully: {opportunity_id}")
|
||||||
|
|
||||||
|
# Retrieve the created opportunity
|
||||||
|
opportunity = await get_opportunity_by_id(opportunity_id, 'test_org')
|
||||||
|
if opportunity:
|
||||||
|
print(f"✓ Opportunity retrieved successfully")
|
||||||
|
print(f" Customer: {opportunity['customer_name']}")
|
||||||
|
print(f" Amount: {opportunity['estimated_amount']}")
|
||||||
|
print(f" Stage: {opportunity['sales_stage']}")
|
||||||
|
else:
|
||||||
|
print("✗ Failed to retrieve opportunity")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error creating opportunity: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_opportunity_listing():
|
||||||
|
"""Test opportunity listing"""
|
||||||
|
print("\nTesting opportunity listing...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
opportunities, total = await list_opportunities('test_org', page=1, page_size=10)
|
||||||
|
print(f"✓ Retrieved {len(opportunities)} opportunities out of {total} total")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error listing opportunities: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main test function"""
|
||||||
|
print("Opportunity Management Module - Integration Test")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
success = True
|
||||||
|
|
||||||
|
# Test opportunity creation
|
||||||
|
success &= await test_opportunity_creation()
|
||||||
|
|
||||||
|
# Test opportunity listing
|
||||||
|
success &= await test_opportunity_listing()
|
||||||
|
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
if success:
|
||||||
|
print("✓ All tests passed! Module is working correctly.")
|
||||||
|
else:
|
||||||
|
print("✗ Some tests failed. Please check the implementation.")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
55
wwwroot/base.ui
Normal file
55
wwwroot/base.ui
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"widgettype": "TabPanel",
|
||||||
|
"options": {
|
||||||
|
"title": "商机管理"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "CRUD",
|
||||||
|
"options": {
|
||||||
|
"title": "商机列表",
|
||||||
|
"url": "{{entire_url(opportunities_list)}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "CRUD",
|
||||||
|
"options": {
|
||||||
|
"title": "销售阶段",
|
||||||
|
"url": "{{entire_url(sales_stages_list)}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Panel",
|
||||||
|
"options": {
|
||||||
|
"title": "销售漏斗分析"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Chart",
|
||||||
|
"options": {
|
||||||
|
"title": "漏斗可视化",
|
||||||
|
"chartType": "funnel",
|
||||||
|
"dataSource": "get_funnel_analysis",
|
||||||
|
"refreshInterval": 30000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Panel",
|
||||||
|
"options": {
|
||||||
|
"title": "销售预测"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Card",
|
||||||
|
"options": {
|
||||||
|
"title": "今日预测汇总",
|
||||||
|
"dataSource": "get_sales_prediction",
|
||||||
|
"fields": ["total_predicted", "avg_confidence", "opportunity_count"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user