From 7f25e71ff3370d3fc346793547f82f72af5cee5a Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 16 Apr 2026 13:29:07 +0800 Subject: [PATCH] bgufix --- README.md | 94 ++++++ customer_management/__init__.py | 0 customer_management/core.py | 453 ++++++++++++++++++++++++++++ customer_management/init.py | 24 ++ init/data.json | 46 +++ json/customer_pool_list.json | 43 +++ json/customers_list.json | 92 ++++++ json/handover_items_list.json | 51 ++++ json/handover_list.json | 55 ++++ models/customer_handover.json | 145 +++++++++ models/customer_handover_items.json | 91 ++++++ models/customer_pool.json | 116 +++++++ models/customers.json | 167 ++++++++++ pyproject.toml | 20 ++ wwwroot/base.ui | 51 ++++ 15 files changed, 1448 insertions(+) create mode 100644 README.md create mode 100644 customer_management/__init__.py create mode 100644 customer_management/core.py create mode 100644 customer_management/init.py create mode 100644 init/data.json create mode 100644 json/customer_pool_list.json create mode 100644 json/customers_list.json create mode 100644 json/handover_items_list.json create mode 100644 json/handover_list.json create mode 100644 models/customer_handover.json create mode 100644 models/customer_handover_items.json create mode 100644 models/customer_pool.json create mode 100644 models/customers.json create mode 100644 pyproject.toml create mode 100644 wwwroot/base.ui diff --git a/README.md b/README.md new file mode 100644 index 0000000..ffe99ee --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/customer_management/__init__.py b/customer_management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/customer_management/core.py b/customer_management/core.py new file mode 100644 index 0000000..7403ad4 --- /dev/null +++ b/customer_management/core.py @@ -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) \ No newline at end of file diff --git a/customer_management/init.py b/customer_management/init.py new file mode 100644 index 0000000..6f6aa2c --- /dev/null +++ b/customer_management/init.py @@ -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) \ No newline at end of file diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..6f78fe9 --- /dev/null +++ b/init/data.json @@ -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": "潜在" + } + ] +} \ No newline at end of file diff --git a/json/customer_pool_list.json b/json/customer_pool_list.json new file mode 100644 index 0000000..f94599a --- /dev/null +++ b/json/customer_pool_list.json @@ -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": "已认领" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/json/customers_list.json b/json/customers_list.json new file mode 100644 index 0000000..fef1740 --- /dev/null +++ b/json/customers_list.json @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/json/handover_items_list.json b/json/handover_items_list.json new file mode 100644 index 0000000..d1ca292 --- /dev/null +++ b/json/handover_items_list.json @@ -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": "未完成" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/json/handover_list.json b/json/handover_list.json new file mode 100644 index 0000000..34f2061 --- /dev/null +++ b/json/handover_list.json @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/models/customer_handover.json b/models/customer_handover.json new file mode 100644 index 0000000..1f74292 --- /dev/null +++ b/models/customer_handover.json @@ -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"] + } + ] +} \ No newline at end of file diff --git a/models/customer_handover_items.json b/models/customer_handover_items.json new file mode 100644 index 0000000..48762c1 --- /dev/null +++ b/models/customer_handover_items.json @@ -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"] + } + ] +} \ No newline at end of file diff --git a/models/customer_pool.json b/models/customer_pool.json new file mode 100644 index 0000000..393d061 --- /dev/null +++ b/models/customer_pool.json @@ -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"] + } + ] +} \ No newline at end of file diff --git a/models/customers.json b/models/customers.json new file mode 100644 index 0000000..71e309d --- /dev/null +++ b/models/customers.json @@ -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"] + } + ] +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9332a0e --- /dev/null +++ b/pyproject.toml @@ -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*"] \ No newline at end of file diff --git a/wwwroot/base.ui b/wwwroot/base.ui new file mode 100644 index 0000000..1f801ca --- /dev/null +++ b/wwwroot/base.ui @@ -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" + } + } + ] + } + ] +} \ No newline at end of file