This commit is contained in:
yumoqing 2026-04-16 13:30:38 +08:00
commit 1b0cc60236
25 changed files with 1845 additions and 0 deletions

83
README.md Normal file
View 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
View 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
View 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": "预测成交金额"
}
]
}
]
}

View 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": "线索转化"}
]
}
}
}
}
}

View 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": "线索转化"}
]
}
}
}
}
}

View File

@ -0,0 +1,11 @@
{
"tblname": "opportunity_predictions",
"alias": "predictions_list",
"title": "商机预测记录",
"params": {
"sortby": ["prediction_date desc"],
"browserfields": {
"exclouded": ["id", "opportunity_id"]
}
}
}

View 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": "预计成交时间"}
]
}
]
}

View 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
View 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"
}
]
}

View 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
View 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"]
}
]
}

View 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"]
}
]
}

View 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
View 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"]
}
]
}

View 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'
]

View 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)

View 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)

View 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
View 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
View 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",
],
)

View 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');

View 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
View 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"]
}
}
]
}
]
}