This commit is contained in:
yumoqing 2026-04-16 13:29:07 +08:00
commit 7f25e71ff3
15 changed files with 1448 additions and 0 deletions

94
README.md Normal file
View File

@ -0,0 +1,94 @@
# 客户管理模块 (Customer Management)
## 模块概述
客户管理模块提供完整的客户档案管理、交接管理和公海池功能支持360度客户视图和自动化工作流程。
## 功能特性
### 2.2 客户管理模块
#### 2.2.1 客户档案管理
- **基础信息**:客户类型(个人/企业)、联系方式、所属行业、分级标签(重要/普通/潜在)
- **360度视图**:整合商机记录、合同历史、服务工单、回款情况,参考纷享销客客户档案体系
- **数据校验**:手机号/企业税号唯一性校验,避免重复建档
#### 2.2.2 客户交接管理
- **触发场景**:人员离职/岗位调整自动触发交接流程
- **交接内容**:含客户基本信息、未结商机、历史合同、未解决问题等核心数据
- **交接流程**
a. 准备阶段:系统自动生成交接清单,原负责人补充完善
b. 审核阶段:负责人审核清单完整性
c. 确认阶段:接手人确认接收,系统自动更新客户负责人
- **客户通知**:交接完成后自动发送短信/邮件告知客户对接人变更
#### 2.2.3 客户公海池
- **规则配置**自动回收N天未跟进的客户
- **分配机制**:手动分配或销售自主领取
## 数据库表结构
### customers (客户档案表)
- `id`: 客户ID (主键)
- `customer_name`: 客户名称 (必填)
- `customer_type`: 客户类型 (individual=个人, enterprise=企业)
- `phone`: 手机号 (唯一性约束)
- `tax_id`: 企业税号 (唯一性约束,仅企业客户)
- `industry`: 所属行业
- `customer_level`: 分级标签 (important=重要, normal=普通, potential=潜在)
- `owner_id`: 负责人ID
- `last_follow_up`: 最后跟进时间
- `status`: 状态 (active=活跃, inactive=非活跃, in_pool=公海)
### customer_handover (客户交接表)
- `id`: 交接ID (主键)
- `customer_id`: 客户ID
- `from_owner_id`: 原负责人ID
- `to_owner_id`: 新负责人ID
- `handover_reason`: 交接原因 (resignation=离职, position_change=岗位调整)
- `current_stage`: 当前阶段 (preparation/review/confirmation/completed)
### customer_handover_items (交接项目明细表)
- `id`: 项目ID (主键)
- `handover_id`: 交接ID
- `item_type`: 项目类型 (basic_info/opportunities/contracts/service_tickets/payment_issues)
- `item_description`: 项目描述
- `is_completed`: 是否完成
### customer_pool (客户公海池表)
- `id`: 公海记录ID (主键)
- `customer_id`: 客户ID
- `original_owner_id`: 原负责人ID
- `recycle_reason`: 回收原因 (inactive_days=未跟进超限, manual=手动)
- `pool_status`: 公海状态 (available/assigned/claimed)
## API接口
### 客户管理
- `create_customer()`: 创建客户档案(包含唯一性校验)
- `get_customer_360_view()`: 获取客户360度视图
### 交接管理
- `initiate_handover()`: 发起交接流程(自动生成清单)
- `complete_handover_preparation()`: 完成准备阶段
- `approve_handover()`: 审核交接清单
- `confirm_handover()`: 确认接收(自动更新负责人并发送通知)
### 公海管理
- `recycle_to_pool()`: 回收客户到公海池
- `claim_from_pool()`: 从公海池认领客户
## 前端界面
- 客户档案CRUD界面
- 客户交接管理界面
- 客户公海池界面
- 客户360度视图查询面板
## 安装部署
1. 将模块目录复制到 `~/repos/` 目录下
2. 运行主应用的 `build.sh` 脚本,自动处理数据库表创建和前端资源链接
3. 模块将自动集成到系统中
## 依赖要求
- ahserver >= 1.0.0
- sqlor-database-module >= 1.0.0
- bricks-framework >= 1.0.0

View File

453
customer_management/core.py Normal file
View File

@ -0,0 +1,453 @@
from datetime import datetime, timedelta
from typing import List, Dict, Optional
import uuid
import re
from ahserver.serverenv import ServerEnv
from appPublic.worker import awaitify
from sqlor.dbp import DBP
async def create_customer(
customer_name: str,
customer_type: str,
phone: str = None,
email: str = None,
tax_id: str = None,
industry: str = None,
customer_level: str = "potential",
address: str = None,
owner_id: str = None,
region: str = None
) -> Dict:
"""创建客户档案"""
dbp = DBP()
customer_id = str(uuid.uuid4()).replace('-', '')
# 数据校验
await validate_customer_data(dbp, phone, tax_id, customer_type)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
customer_data = {
"id": customer_id,
"customer_name": customer_name,
"customer_type": customer_type,
"phone": phone,
"email": email,
"tax_id": tax_id,
"industry": industry,
"customer_level": customer_level,
"address": address,
"owner_id": owner_id or get_current_user_id(),
"region": region,
"last_follow_up": now,
"created_at": now,
"updated_at": now,
"status": "active"
}
await dbp.insert("customers", customer_data)
return customer_data
async def validate_customer_data(dbp, phone: str, tax_id: str, customer_type: str):
"""验证客户数据唯一性"""
# 手机号唯一性校验
if phone:
existing_phone = await dbp.select_one("customers", {"phone": phone})
if existing_phone:
raise ValueError(f"手机号 {phone} 已存在,不能重复建档")
# 企业税号唯一性校验(仅对企业客户)
if customer_type == "enterprise" and tax_id:
existing_tax = await dbp.select_one("customers", {"tax_id": tax_id})
if existing_tax:
raise ValueError(f"企业税号 {tax_id} 已存在,不能重复建档")
async def initiate_handover(
customer_id: str,
to_owner_id: str,
handover_reason: str,
reviewer_id: str = None
) -> Dict:
"""发起客户交接流程"""
dbp = DBP()
# 获取客户信息
customer = await dbp.select_one("customers", {"id": customer_id})
if not customer:
raise ValueError("客户不存在")
if customer["status"] != "active":
raise ValueError("只能交接活跃状态的客户")
# 创建交接记录
handover_id = str(uuid.uuid4()).replace('-', '')
handover_data = {
"id": handover_id,
"customer_id": customer_id,
"from_owner_id": customer["owner_id"],
"to_owner_id": to_owner_id,
"handover_reason": handover_reason,
"current_stage": "preparation",
"reviewer_id": reviewer_id,
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
await dbp.insert("customer_handover", handover_data)
# 自动生成交接清单
await generate_handover_items(dbp, handover_id, customer_id)
return handover_data
async def generate_handover_items(dbp, handover_id: str, customer_id: str):
"""自动生成交接清单"""
items = []
# 基本信息
items.append({
"id": str(uuid.uuid4()).replace('-', ''),
"handover_id": handover_id,
"item_type": "basic_info",
"item_description": "客户基本信息和联系记录",
"is_completed": "0",
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
})
# 未结商机
opportunities = await dbp.query(
"SELECT id, customer_name, estimated_amount, current_stage FROM opportunities WHERE customer_name = (SELECT customer_name FROM customers WHERE id = %(customer_id)s) AND status = 'active'",
{"customer_id": customer_id}
)
if opportunities:
for opp in opportunities:
items.append({
"id": str(uuid.uuid4()).replace('-', ''),
"handover_id": handover_id,
"item_type": "opportunities",
"item_id": opp["id"],
"item_description": f"商机: {opp['customer_name']} - 预估金额: {opp['estimated_amount']}, 阶段: {opp['current_stage']}",
"is_completed": "0",
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
})
# 历史合同(假设合同管理模块存在)
contracts = await dbp.query(
"SELECT id, contract_no, amount, status FROM contracts WHERE customer_id = %(customer_id)s",
{"customer_id": customer_id}
)
if contracts:
for contract in contracts:
items.append({
"id": str(uuid.uuid4()).replace('-', ''),
"handover_id": handover_id,
"item_type": "contracts",
"item_id": contract["id"],
"item_description": f"合同: {contract['contract_no']} - 金额: {contract['amount']}, 状态: {contract['status']}",
"is_completed": "0",
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
})
# 服务工单(假设服务模块存在)
service_tickets = await dbp.query(
"SELECT id, ticket_no, subject, status FROM service_tickets WHERE customer_id = %(customer_id)s AND status != 'closed'",
{"customer_id": customer_id}
)
if service_tickets:
for ticket in service_tickets:
items.append({
"id": str(uuid.uuid4()).replace('-', ''),
"handover_id": handover_id,
"item_type": "service_tickets",
"item_id": ticket["id"],
"item_description": f"服务工单: {ticket['ticket_no']} - 主题: {ticket['subject']}, 状态: {ticket['status']}",
"is_completed": "0",
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
})
# 未解决回款问题
payment_issues = await dbp.query(
"SELECT id, invoice_no, amount, due_date FROM payment_records WHERE customer_id = %(customer_id)s AND status = 'overdue'",
{"customer_id": customer_id}
)
if payment_issues:
for issue in payment_issues:
items.append({
"id": str(uuid.uuid4()).replace('-', ''),
"handover_id": handover_id,
"item_type": "payment_issues",
"item_id": issue["id"],
"item_description": f"回款问题: 发票 {issue['invoice_no']} - 金额: {issue['amount']}, 到期日: {issue['due_date']}",
"is_completed": "0",
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
})
# 批量插入交接项目
for item in items:
await dbp.insert("customer_handover_items", item)
async def complete_handover_preparation(handover_id: str) -> Dict:
"""完成交接准备阶段"""
dbp = DBP()
handover = await dbp.select_one("customer_handover", {"id": handover_id})
if not handover:
raise ValueError("交接记录不存在")
if handover["current_stage"] != "preparation":
raise ValueError("当前不在准备阶段")
# 更新为审核阶段
await dbp.update(
"customer_handover",
{
"current_stage": "review",
"prepared_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{"id": handover_id}
)
return {"handover_id": handover_id, "stage": "review"}
async def approve_handover(handover_id: str, approver_id: str = None) -> Dict:
"""审核交接清单"""
dbp = DBP()
approver_id = approver_id or get_current_user_id()
handover = await dbp.select_one("customer_handover", {"id": handover_id})
if not handover:
raise ValueError("交接记录不存在")
if handover["current_stage"] != "review":
raise ValueError("当前不在审核阶段")
# 更新为确认阶段
await dbp.update(
"customer_handover",
{
"current_stage": "confirmation",
"reviewed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{"id": handover_id}
)
return {"handover_id": handover_id, "stage": "confirmation"}
async def confirm_handover(handover_id: str, confirm_by: str = None) -> Dict:
"""确认接收客户"""
dbp = DBP()
confirm_by = confirm_by or get_current_user_id()
handover = await dbp.select_one("customer_handover", {"id": handover_id})
if not handover:
raise ValueError("交接记录不存在")
if handover["current_stage"] != "confirmation":
raise ValueError("当前不在确认阶段")
# 更新客户负责人
await dbp.update(
"customers",
{
"owner_id": handover["to_owner_id"],
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{"id": handover["customer_id"]}
)
# 完成交接流程
await dbp.update(
"customer_handover",
{
"current_stage": "completed",
"confirmed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"completed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{"id": handover_id}
)
# 发送客户通知(模拟)
await send_customer_notification(handover["customer_id"], handover["to_owner_id"])
return {"handover_id": handover_id, "stage": "completed", "customer_id": handover["customer_id"]}
async def send_customer_notification(customer_id: str, new_owner_id: str):
"""发送客户对接人变更通知"""
# 这里应该集成短信/邮件服务
# 模拟实现
dbp = DBP()
customer = await dbp.select_one("customers", {"id": customer_id})
new_owner = await dbp.select_one("users", {"id": new_owner_id}) # 假设用户表存在
notification_content = f"尊敬的{customer['customer_name']},您的客户经理已变更为{new_owner.get('name', '新经理')},联系方式:{new_owner.get('phone', '待更新')}"
# 记录通知日志(实际应调用短信/邮件API
print(f"客户通知已发送: {notification_content}")
async def recycle_to_pool(customer_id: str, inactive_days: int = None, reason: str = "inactive_days"):
"""回收客户到公海池"""
dbp = DBP()
customer = await dbp.select_one("customers", {"id": customer_id})
if not customer:
raise ValueError("客户不存在")
if customer["status"] == "in_pool":
raise ValueError("客户已在公海池中")
# 创建公海记录
pool_id = str(uuid.uuid4()).replace('-', '')
pool_data = {
"id": pool_id,
"customer_id": customer_id,
"original_owner_id": customer["owner_id"],
"recycle_reason": reason,
"inactive_days": inactive_days,
"recycled_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"pool_status": "available",
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
await dbp.insert("customer_pool", pool_data)
# 更新客户状态
await dbp.update(
"customers",
{
"status": "in_pool",
"owner_id": "", # 清空负责人
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{"id": customer_id}
)
return pool_data
async def claim_from_pool(pool_id: str, new_owner_id: str = None):
"""从公海池认领客户"""
dbp = DBP()
new_owner_id = new_owner_id or get_current_user_id()
pool_record = await dbp.select_one("customer_pool", {"id": pool_id})
if not pool_record:
raise ValueError("公海记录不存在")
if pool_record["pool_status"] != "available":
raise ValueError("该客户已被认领或分配")
# 更新公海记录
await dbp.update(
"customer_pool",
{
"assigned_to": new_owner_id,
"assigned_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"pool_status": "claimed",
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{"id": pool_id}
)
# 更新客户状态和负责人
await dbp.update(
"customers",
{
"status": "active",
"owner_id": new_owner_id,
"last_follow_up": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{"id": pool_record["customer_id"]}
)
return {"customer_id": pool_record["customer_id"], "new_owner_id": new_owner_id}
async def get_customer_360_view(customer_id: str) -> Dict:
"""获取客户360度视图"""
dbp = DBP()
# 客户基本信息
customer = await dbp.select_one("customers", {"id": customer_id})
if not customer:
raise ValueError("客户不存在")
# 商机记录
opportunities = await dbp.query(
"SELECT id, estimated_amount, current_stage, expected_close_date, status FROM opportunities WHERE customer_name = %(customer_name)s ORDER BY created_at DESC",
{"customer_name": customer["customer_name"]}
)
# 合同历史(假设合同管理模块存在)
contracts = await dbp.query(
"SELECT id, contract_no, amount, start_date, end_date, status FROM contracts WHERE customer_id = %(customer_id)s ORDER BY created_at DESC",
{"customer_id": customer_id}
)
# 服务工单(假设服务模块存在)
service_tickets = await dbp.query(
"SELECT id, ticket_no, subject, priority, status, created_at FROM service_tickets WHERE customer_id = %(customer_id)s ORDER BY created_at DESC",
{"customer_id": customer_id}
)
# 回款情况
payments = await dbp.query(
"SELECT id, invoice_no, amount, paid_amount, due_date, status FROM payment_records WHERE customer_id = %(customer_id)s ORDER BY due_date DESC",
{"customer_id": customer_id}
)
return {
"customer": customer,
"opportunities": opportunities,
"contracts": contracts,
"service_tickets": service_tickets,
"payments": payments
}
def get_current_user_id() -> str:
"""获取当前用户ID模拟实现"""
return "current_user_id"
# 同步版本函数
def sync_create_customer(*args, **kwargs):
return create_customer(*args, **kwargs)
def sync_initiate_handover(*args, **kwargs):
return initiate_handover(*args, **kwargs)
def sync_complete_handover_preparation(*args, **kwargs):
return complete_handover_preparation(*args, **kwargs)
def sync_approve_handover(*args, **kwargs):
return approve_handover(*args, **kwargs)
def sync_confirm_handover(*args, **kwargs):
return confirm_handover(*args, **kwargs)
def sync_recycle_to_pool(*args, **kwargs):
return recycle_to_pool(*args, **kwargs)
def sync_claim_from_pool(*args, **kwargs):
return claim_from_pool(*args, **kwargs)
def sync_get_customer_360_view(*args, **kwargs):
return get_customer_360_view(*args, **kwargs)

View File

@ -0,0 +1,24 @@
from ahserver.serverenv import ServerEnv
from appPublic.worker import awaitify
from .core import (
sync_create_customer,
sync_initiate_handover,
sync_complete_handover_preparation,
sync_approve_handover,
sync_confirm_handover,
sync_recycle_to_pool,
sync_claim_from_pool,
sync_get_customer_360_view
)
def load_customer_management():
env = ServerEnv()
env.create_customer = awaitify(sync_create_customer)
env.initiate_handover = awaitify(sync_initiate_handover)
env.complete_handover_preparation = awaitify(sync_complete_handover_preparation)
env.approve_handover = awaitify(sync_approve_handover)
env.confirm_handover = awaitify(sync_confirm_handover)
env.recycle_to_pool = awaitify(sync_recycle_to_pool)
env.claim_from_pool = awaitify(sync_claim_from_pool)
env.get_customer_360_view = awaitify(sync_get_customer_360_view)

46
init/data.json Normal file
View File

@ -0,0 +1,46 @@
{
"appcodes": [
{
"id": "customer_type",
"name": "客户类型编码",
"hierarchy_flg": "0"
},
{
"id": "customer_level",
"name": "客户分级编码",
"hierarchy_flg": "0"
}
],
"appcodes_kv": [
{
"id": "customer_type_individual",
"parentid": "customer_type",
"k": "individual",
"v": "个人"
},
{
"id": "customer_type_enterprise",
"parentid": "customer_type",
"k": "enterprise",
"v": "企业"
},
{
"id": "customer_level_important",
"parentid": "customer_level",
"k": "important",
"v": "重要"
},
{
"id": "customer_level_normal",
"parentid": "customer_level",
"k": "normal",
"v": "普通"
},
{
"id": "customer_level_potential",
"parentid": "customer_level",
"k": "potential",
"v": "潜在"
}
]
}

View File

@ -0,0 +1,43 @@
{
"tblname": "customer_pool",
"alias": "customer_pool_list",
"title": "客户公海池",
"params": {
"sortby": ["recycled_at desc"],
"browserfields": {
"exclouded": ["id", "customer_id", "original_owner_id", "assigned_to", "created_at"],
"alters": {
"recycle_reason": {
"uitype": "code",
"data": [
{
"value": "inactive_days",
"text": "未跟进天数超限"
},
{
"value": "manual",
"text": "手动回收"
}
]
},
"pool_status": {
"uitype": "code",
"data": [
{
"value": "available",
"text": "可领取"
},
{
"value": "assigned",
"text": "已分配"
},
{
"value": "claimed",
"text": "已认领"
}
]
}
}
}
}
}

92
json/customers_list.json Normal file
View File

@ -0,0 +1,92 @@
{
"tblname": "customers",
"alias": "customers_list",
"title": "客户档案管理",
"params": {
"sortby": ["created_at desc"],
"logined_userid": "owner_id",
"browserfields": {
"exclouded": ["id", "owner_id", "updated_at"],
"alters": {
"customer_type": {
"uitype": "code",
"data": [
{
"value": "individual",
"text": "个人"
},
{
"value": "enterprise",
"text": "企业"
}
]
},
"customer_level": {
"uitype": "code",
"data": [
{
"value": "important",
"text": "重要"
},
{
"value": "normal",
"text": "普通"
},
{
"value": "potential",
"text": "潜在"
}
]
},
"status": {
"uitype": "code",
"data": [
{
"value": "active",
"text": "活跃"
},
{
"value": "inactive",
"text": "非活跃"
},
{
"value": "in_pool",
"text": "公海"
}
]
}
}
},
"editor": {
"binds": [
{
"wid": "customer_type",
"event": "changed",
"actiontype": "script",
"target": "tax_id",
"script": "// 当客户类型为个人时,隐藏税号字段\nconst customerType = this.getValue('customer_type');\nconst taxIdField = this.getWidget('tax_id');\nif (customerType === 'individual') {\n taxIdField.hide();\n} else {\n taxIdField.show();\n}"
}
]
},
"subtables": [
{
"field": "customer_id",
"title": "客户360度视图",
"url": "{{entire_url(customer_360_view)}}",
"subtable": "customers"
},
{
"field": "customer_id",
"title": "交接记录",
"url": "{{entire_url(handover_history_list)}}",
"subtable": "customer_handover"
},
{
"field": "customer_id",
"title": "公海记录",
"url": "{{entire_url(pool_history_list)}}",
"subtable": "customer_pool"
}
]
}
}

View File

@ -0,0 +1,51 @@
{
"tblname": "customer_handover_items",
"alias": "handover_items_list",
"title": "交接项目明细",
"params": {
"sortby": ["created_at desc"],
"browserfields": {
"exclouded": ["id", "handover_id", "item_id", "updated_at"],
"alters": {
"item_type": {
"uitype": "code",
"data": [
{
"value": "basic_info",
"text": "基本信息"
},
{
"value": "opportunities",
"text": "未结商机"
},
{
"value": "contracts",
"text": "历史合同"
},
{
"value": "service_tickets",
"text": "服务工单"
},
{
"value": "payment_issues",
"text": "回款问题"
}
]
},
"is_completed": {
"uitype": "code",
"data": [
{
"value": "1",
"text": "已完成"
},
{
"value": "0",
"text": "未完成"
}
]
}
}
}
}
}

55
json/handover_list.json Normal file
View File

@ -0,0 +1,55 @@
{
"tblname": "customer_handover",
"alias": "handover_list",
"title": "客户交接管理",
"params": {
"sortby": ["created_at desc"],
"browserfields": {
"exclouded": ["id", "from_owner_id", "to_owner_id", "reviewer_id", "updated_at"],
"alters": {
"handover_reason": {
"uitype": "code",
"data": [
{
"value": "resignation",
"text": "人员离职"
},
{
"value": "position_change",
"text": "岗位调整"
}
]
},
"current_stage": {
"uitype": "code",
"data": [
{
"value": "preparation",
"text": "准备阶段"
},
{
"value": "review",
"text": "审核阶段"
},
{
"value": "confirmation",
"text": "确认阶段"
},
{
"value": "completed",
"text": "已完成"
}
]
}
}
},
"subtables": [
{
"field": "handover_id",
"title": "交接项目明细",
"url": "{{entire_url(handover_items_list)}}",
"subtable": "customer_handover_items"
}
]
}
}

View File

@ -0,0 +1,145 @@
{
"summary": [
{
"name": "customer_handover",
"title": "客户交接表",
"primary": "id",
"catelog": "relation"
}
],
"fields": [
{
"name": "id",
"title": "交接ID",
"type": "str",
"length": 32,
"nullable": "no",
"comments": "主键 - UUID格式"
},
{
"name": "customer_id",
"title": "客户ID",
"type": "str",
"length": 32,
"nullable": "no",
"comments": "被交接的客户ID"
},
{
"name": "from_owner_id",
"title": "原负责人ID",
"type": "str",
"length": 32,
"nullable": "no",
"comments": "原客户负责人ID"
},
{
"name": "to_owner_id",
"title": "新负责人ID",
"type": "str",
"length": 32,
"nullable": "no",
"comments": "新客户负责人ID"
},
{
"name": "handover_reason",
"title": "交接原因",
"type": "str",
"length": 100,
"nullable": "no",
"comments": "交接触发原因resignation=离职, position_change=岗位调整"
},
{
"name": "current_stage",
"title": "当前阶段",
"type": "str",
"length": 20,
"nullable": "no",
"default": "preparation",
"comments": "交接流程阶段preparation=准备, review=审核, confirmation=确认, completed=完成"
},
{
"name": "reviewer_id",
"title": "审核人ID",
"type": "str",
"length": 32,
"nullable": "yes",
"comments": "负责审核交接清单的人员ID"
},
{
"name": "prepared_at",
"title": "准备完成时间",
"type": "timestamp",
"nullable": "yes",
"comments": "原负责人完成交接清单准备的时间"
},
{
"name": "reviewed_at",
"title": "审核完成时间",
"type": "timestamp",
"nullable": "yes",
"comments": "审核人完成审核的时间"
},
{
"name": "confirmed_at",
"title": "确认完成时间",
"type": "timestamp",
"nullable": "yes",
"comments": "接手人确认接收的时间"
},
{
"name": "completed_at",
"title": "交接完成时间",
"type": "timestamp",
"nullable": "yes",
"comments": "整个交接流程完成的时间"
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp",
"nullable": "no",
"comments": "交接流程创建时间"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp",
"nullable": "no",
"comments": "最后更新时间"
},
{
"name": "notes",
"title": "备注",
"type": "text",
"nullable": "yes",
"comments": "交接过程中的备注信息"
}
],
"indexes": [
{
"name": "idx_handover_customer",
"idxtype": "index",
"idxfields": ["customer_id"]
},
{
"name": "idx_handover_from_owner",
"idxtype": "index",
"idxfields": ["from_owner_id"]
},
{
"name": "idx_handover_to_owner",
"idxtype": "index",
"idxfields": ["to_owner_id"]
},
{
"name": "idx_handover_stage",
"idxtype": "index",
"idxfields": ["current_stage"]
},
{
"name": "idx_handover_created",
"idxtype": "index",
"idxfields": ["created_at"]
}
]
}

View File

@ -0,0 +1,91 @@
{
"summary": [
{
"name": "customer_handover_items",
"title": "客户交接项目明细表",
"primary": "id",
"catelog": "relation"
}
],
"fields": [
{
"name": "id",
"title": "项目ID",
"type": "str",
"length": 32,
"nullable": "no",
"comments": "主键 - UUID格式"
},
{
"name": "handover_id",
"title": "交接ID",
"type": "str",
"length": 32,
"nullable": "no",
"comments": "关联的交接记录ID"
},
{
"name": "item_type",
"title": "项目类型",
"type": "str",
"length": 50,
"nullable": "no",
"comments": "交接项目类型basic_info=基本信息, opportunities=未结商机, contracts=历史合同, service_tickets=服务工单, payment_issues=回款问题"
},
{
"name": "item_id",
"title": "关联ID",
"type": "str",
"length": 32,
"nullable": "yes",
"comments": "关联的具体记录ID如商机ID、合同ID等"
},
{
"name": "item_description",
"title": "项目描述",
"type": "text",
"nullable": "no",
"comments": "项目详细描述或补充说明"
},
{
"name": "is_completed",
"title": "是否完成",
"type": "str",
"length": 1,
"nullable": "no",
"default": "0",
"comments": "是否已完成交接1=是, 0=否"
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp",
"nullable": "no",
"comments": "项目创建时间"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp",
"nullable": "no",
"comments": "最后更新时间"
}
],
"indexes": [
{
"name": "idx_handover_items_handover",
"idxtype": "index",
"idxfields": ["handover_id"]
},
{
"name": "idx_handover_items_type",
"idxtype": "index",
"idxfields": ["item_type"]
},
{
"name": "idx_handover_items_item_id",
"idxtype": "index",
"idxfields": ["item_id"]
}
]
}

116
models/customer_pool.json Normal file
View File

@ -0,0 +1,116 @@
{
"summary": [
{
"name": "customer_pool",
"title": "客户公海池表",
"primary": "id",
"catelog": "relation"
}
],
"fields": [
{
"name": "id",
"title": "公海记录ID",
"type": "str",
"length": 32,
"nullable": "no",
"comments": "主键 - UUID格式"
},
{
"name": "customer_id",
"title": "客户ID",
"type": "str",
"length": 32,
"nullable": "no",
"comments": "回收到公海的客户ID"
},
{
"name": "original_owner_id",
"title": "原负责人ID",
"type": "str",
"length": 32,
"nullable": "no",
"comments": "客户原来的负责人ID"
},
{
"name": "recycle_reason",
"title": "回收原因",
"type": "str",
"length": 100,
"nullable": "no",
"comments": "回收原因inactive_days=未跟进天数超限, manual=手动回收"
},
{
"name": "inactive_days",
"title": "未跟进天数",
"type": "long",
"nullable": "yes",
"comments": "触发回收的未跟进天数"
},
{
"name": "recycled_at",
"title": "回收时间",
"type": "timestamp",
"nullable": "no",
"comments": "客户被回收到公海的时间"
},
{
"name": "assigned_to",
"title": "分配给",
"type": "str",
"length": 32,
"nullable": "yes",
"comments": "分配给的新负责人ID如果已分配"
},
{
"name": "assigned_at",
"title": "分配时间",
"type": "timestamp",
"nullable": "yes",
"comments": "客户被分配的时间"
},
{
"name": "pool_status",
"title": "公海状态",
"type": "str",
"length": 20,
"nullable": "no",
"default": "available",
"comments": "公海状态available=可领取, assigned=已分配, claimed=已认领"
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp",
"nullable": "no",
"comments": "记录创建时间"
}
],
"indexes": [
{
"name": "idx_pool_customer",
"idxtype": "unique",
"idxfields": ["customer_id"]
},
{
"name": "idx_pool_original_owner",
"idxtype": "index",
"idxfields": ["original_owner_id"]
},
{
"name": "idx_pool_assigned_to",
"idxtype": "index",
"idxfields": ["assigned_to"]
},
{
"name": "idx_pool_status",
"idxtype": "index",
"idxfields": ["pool_status"]
},
{
"name": "idx_pool_recycled",
"idxtype": "index",
"idxfields": ["recycled_at"]
}
]
}

167
models/customers.json Normal file
View File

@ -0,0 +1,167 @@
{
"summary": [
{
"name": "customers",
"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": "customer_type",
"title": "客户类型",
"type": "str",
"length": 20,
"nullable": "no",
"comments": "客户类型individual=个人, enterprise=企业"
},
{
"name": "phone",
"title": "手机号",
"type": "str",
"length": 20,
"nullable": "yes",
"comments": "客户手机号码"
},
{
"name": "email",
"title": "邮箱",
"type": "str",
"length": 255,
"nullable": "yes",
"comments": "客户邮箱地址"
},
{
"name": "tax_id",
"title": "企业税号",
"type": "str",
"length": 50,
"nullable": "yes",
"comments": "企业统一社会信用代码或税号"
},
{
"name": "industry",
"title": "所属行业",
"type": "str",
"length": 100,
"nullable": "yes",
"comments": "客户所属行业"
},
{
"name": "customer_level",
"title": "分级标签",
"type": "str",
"length": 20,
"nullable": "no",
"default": "potential",
"comments": "客户分级important=重要, normal=普通, potential=潜在"
},
{
"name": "address",
"title": "地址",
"type": "text",
"nullable": "yes",
"comments": "客户详细地址"
},
{
"name": "owner_id",
"title": "负责人ID",
"type": "str",
"length": 32,
"nullable": "no",
"comments": "当前负责该客户的销售人员ID"
},
{
"name": "region",
"title": "区域",
"type": "str",
"length": 100,
"nullable": "yes",
"comments": "客户所在区域"
},
{
"name": "last_follow_up",
"title": "最后跟进时间",
"type": "timestamp",
"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=活跃, inactive=非活跃, in_pool=公海"
}
],
"indexes": [
{
"name": "idx_customers_phone",
"idxtype": "unique",
"idxfields": ["phone"]
},
{
"name": "idx_customers_tax_id",
"idxtype": "unique",
"idxfields": ["tax_id"]
},
{
"name": "idx_customers_owner",
"idxtype": "index",
"idxfields": ["owner_id"]
},
{
"name": "idx_customers_name",
"idxtype": "index",
"idxfields": ["customer_name"]
},
{
"name": "idx_customers_level",
"idxtype": "index",
"idxfields": ["customer_level"]
},
{
"name": "idx_customers_status",
"idxtype": "index",
"idxfields": ["status"]
},
{
"name": "idx_customers_last_follow",
"idxtype": "index",
"idxfields": ["last_follow_up"]
}
]
}

20
pyproject.toml Normal file
View File

@ -0,0 +1,20 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "customer_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 = ["customer_management*"]

51
wwwroot/base.ui Normal file
View File

@ -0,0 +1,51 @@
{
"widgettype": "TabPanel",
"options": {
"title": "客户管理"
},
"subwidgets": [
{
"widgettype": "CRUD",
"options": {
"title": "客户档案",
"url": "{{entire_url(customers_list)}}"
}
},
{
"widgettype": "CRUD",
"options": {
"title": "客户交接",
"url": "{{entire_url(handover_list)}}"
}
},
{
"widgettype": "CRUD",
"options": {
"title": "客户公海池",
"url": "{{entire_url(customer_pool_list)}}"
}
},
{
"widgettype": "Panel",
"options": {
"title": "客户360度视图"
},
"subwidgets": [
{
"widgettype": "Form",
"options": {
"title": "客户查询",
"fields": [
{
"name": "customer_id",
"label": "客户ID",
"type": "text"
}
],
"onSubmit": "get_customer_360_view"
}
}
]
}
]
}