sync: local modifications to customer_management

- Updated core.py
- Updated base.ui
- Added new API files: check_tables, customer_pool_list, customers CRUD, handover_list
- Added new UI/DSPY files: customer_edit, customer_list, customer_pool, handover_list
This commit is contained in:
yumoqing 2026-04-28 18:51:23 +08:00
parent 12e2fb6978
commit 469255afe7
13 changed files with 752 additions and 322 deletions

View File

@ -5,7 +5,7 @@ import re
from ahserver.serverenv import ServerEnv
from appPublic.worker import awaitify
from sqlor.dbp import DBP
from sqlor.dbpools import DBPools
async def create_customer(
@ -21,11 +21,12 @@ async def create_customer(
region: str = None
) -> Dict:
"""创建客户档案"""
dbp = DBP()
db = DBPools()
async with db.sqlorContext('default') as sor:
customer_id = str(uuid.uuid4()).replace('-', '')
# 数据校验
await validate_customer_data(dbp, phone, tax_id, customer_type)
await validate_customer_data(sor, phone, tax_id, customer_type)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
customer_data = {
@ -46,22 +47,22 @@ async def create_customer(
"status": "active"
}
await dbp.insert("customers", customer_data)
await sor.C("customers", customer_data)
return customer_data
async def validate_customer_data(dbp, phone: str, tax_id: str, customer_type: str):
async def validate_customer_data(sor, phone: str, tax_id: str, customer_type: str):
"""验证客户数据唯一性"""
# 手机号唯一性校验
if phone:
existing_phone = await dbp.select_one("customers", {"phone": phone})
if existing_phone:
existing_phone = await sor.R("customers", {"filters": [{"field": "phone", "op": "=", "value": phone}]})
if existing_phone and len(existing_phone) > 0:
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:
existing_tax = await sor.R("customers", {"filters": [{"field": "tax_id", "op": "=", "value": tax_id}]})
if existing_tax and len(existing_tax) > 0:
raise ValueError(f"企业税号 {tax_id} 已存在,不能重复建档")
@ -72,13 +73,14 @@ async def initiate_handover(
reviewer_id: str = None
) -> Dict:
"""发起客户交接流程"""
dbp = DBP()
db = DBPools()
async with db.sqlorContext('default') as sor:
# 获取客户信息
customer = await dbp.select_one("customers", {"id": customer_id})
if not customer:
customer_records = await sor.R("customers", {"filters": [{"field": "id", "op": "=", "value": customer_id}]})
if not customer_records or len(customer_records) == 0:
raise ValueError("客户不存在")
customer = customer_records[0]
if customer["status"] != "active":
raise ValueError("只能交接活跃状态的客户")
@ -96,15 +98,15 @@ async def initiate_handover(
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
await dbp.insert("customer_handover", handover_data)
await sor.C("customer_handover", handover_data)
# 自动生成交接清单
await generate_handover_items(dbp, handover_id, customer_id)
await generate_handover_items(sor, handover_id, customer_id)
return handover_data
async def generate_handover_items(dbp, handover_id: str, customer_id: str):
async def generate_handover_items(sor, handover_id: str, customer_id: str):
"""自动生成交接清单"""
items = []
@ -120,10 +122,12 @@ async def generate_handover_items(dbp, handover_id: str, customer_id: str):
})
# 未结商机
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}
)
opportunities = await sor.R("opportunities", {
"filters": [
{"field": "customer_name", "op": "=", "value": f"(SELECT customer_name FROM customers WHERE id = '{customer_id}')"},
{"field": "status", "op": "=", "value": "active"}
]
})
if opportunities:
for opp in opportunities:
items.append({
@ -138,10 +142,7 @@ async def generate_handover_items(dbp, handover_id: str, customer_id: str):
})
# 历史合同(假设合同管理模块存在)
contracts = await dbp.query(
"SELECT id, contract_no, amount, status FROM contracts WHERE customer_id = %(customer_id)s",
{"customer_id": customer_id}
)
contracts = await sor.R("contracts", {"filters": [{"field": "customer_id", "op": "=", "value": customer_id}]})
if contracts:
for contract in contracts:
items.append({
@ -156,10 +157,12 @@ async def generate_handover_items(dbp, handover_id: str, customer_id: str):
})
# 服务工单(假设服务模块存在)
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}
)
service_tickets = await sor.R("service_tickets", {
"filters": [
{"field": "customer_id", "op": "=", "value": customer_id},
{"field": "status", "op": "!=", "value": "closed"}
]
})
if service_tickets:
for ticket in service_tickets:
items.append({
@ -174,10 +177,12 @@ async def generate_handover_items(dbp, handover_id: str, customer_id: str):
})
# 未解决回款问题
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}
)
payment_issues = await sor.R("payment_records", {
"filters": [
{"field": "customer_id", "op": "=", "value": customer_id},
{"field": "status", "op": "=", "value": "overdue"}
]
})
if payment_issues:
for issue in payment_issues:
items.append({
@ -193,29 +198,30 @@ async def generate_handover_items(dbp, handover_id: str, customer_id: str):
# 批量插入交接项目
for item in items:
await dbp.insert("customer_handover_items", item)
await sor.C("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:
db = DBPools()
async with db.sqlorContext('default') as sor:
handover_records = await sor.R("customer_handover", {"filters": [{"field": "id", "op": "=", "value": handover_id}]})
if not handover_records or len(handover_records) == 0:
raise ValueError("交接记录不存在")
handover = handover_records[0]
if handover["current_stage"] != "preparation":
raise ValueError("当前不在准备阶段")
# 更新为审核阶段
await dbp.update(
await sor.U(
"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}
{"filters": [{"field": "id", "op": "=", "value": handover_id}]}
)
return {"handover_id": handover_id, "stage": "review"}
@ -223,25 +229,27 @@ async def complete_handover_preparation(handover_id: str) -> Dict:
async def approve_handover(handover_id: str, approver_id: str = None) -> Dict:
"""审核交接清单"""
dbp = DBP()
db = DBPools()
async with db.sqlorContext('default') as sor:
approver_id = approver_id or get_current_user_id()
handover = await dbp.select_one("customer_handover", {"id": handover_id})
if not handover:
handover_records = await sor.R("customer_handover", {"filters": [{"field": "id", "op": "=", "value": handover_id}]})
if not handover_records or len(handover_records) == 0:
raise ValueError("交接记录不存在")
handover = handover_records[0]
if handover["current_stage"] != "review":
raise ValueError("当前不在审核阶段")
# 更新为确认阶段
await dbp.update(
await sor.U(
"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}
{"filters": [{"field": "id", "op": "=", "value": handover_id}]}
)
return {"handover_id": handover_id, "stage": "confirmation"}
@ -249,28 +257,30 @@ async def approve_handover(handover_id: str, approver_id: str = None) -> Dict:
async def confirm_handover(handover_id: str, confirm_by: str = None) -> Dict:
"""确认接收客户"""
dbp = DBP()
db = DBPools()
async with db.sqlorContext('default') as sor:
confirm_by = confirm_by or get_current_user_id()
handover = await dbp.select_one("customer_handover", {"id": handover_id})
if not handover:
handover_records = await sor.R("customer_handover", {"filters": [{"field": "id", "op": "=", "value": handover_id}]})
if not handover_records or len(handover_records) == 0:
raise ValueError("交接记录不存在")
handover = handover_records[0]
if handover["current_stage"] != "confirmation":
raise ValueError("当前不在确认阶段")
# 更新客户负责人
await dbp.update(
await sor.U(
"customers",
{
"owner_id": handover["to_owner_id"],
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{"id": handover["customer_id"]}
{"filters": [{"field": "id", "op": "=", "value": handover["customer_id"]}]}
)
# 完成交接流程
await dbp.update(
await sor.U(
"customer_handover",
{
"current_stage": "completed",
@ -278,23 +288,26 @@ async def confirm_handover(handover_id: str, confirm_by: str = None) -> Dict:
"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}
{"filters": [{"field": "id", "op": "=", "value": handover_id}]}
)
# 发送客户通知(模拟)
await send_customer_notification(handover["customer_id"], handover["to_owner_id"])
await send_customer_notification(sor, 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):
async def send_customer_notification(sor, 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}) # 假设用户表存在
customer_records = await sor.R("customers", {"filters": [{"field": "id", "op": "=", "value": customer_id}]})
customer = customer_records[0] if customer_records else None
new_owner_records = await sor.R("users", {"filters": [{"field": "id", "op": "=", "value": new_owner_id}]})
new_owner = new_owner_records[0] if new_owner_records else {"name": "新经理", "phone": "待更新"}
if customer:
notification_content = f"尊敬的{customer['customer_name']},您的客户经理已变更为{new_owner.get('name', '新经理')},联系方式:{new_owner.get('phone', '待更新')}"
# 记录通知日志(实际应调用短信/邮件API
@ -303,12 +316,13 @@ async def send_customer_notification(customer_id: str, new_owner_id: str):
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:
db = DBPools()
async with db.sqlorContext('default') as sor:
customer_records = await sor.R("customers", {"filters": [{"field": "id", "op": "=", "value": customer_id}]})
if not customer_records or len(customer_records) == 0:
raise ValueError("客户不存在")
customer = customer_records[0]
if customer["status"] == "in_pool":
raise ValueError("客户已在公海池中")
@ -325,17 +339,17 @@ async def recycle_to_pool(customer_id: str, inactive_days: int = None, reason: s
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
await dbp.insert("customer_pool", pool_data)
await sor.C("customer_pool", pool_data)
# 更新客户状态
await dbp.update(
await sor.U(
"customers",
{
"status": "in_pool",
"owner_id": "", # 清空负责人
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{"id": customer_id}
{"filters": [{"field": "id", "op": "=", "value": customer_id}]}
)
return pool_data
@ -343,18 +357,20 @@ async def recycle_to_pool(customer_id: str, inactive_days: int = None, reason: s
async def claim_from_pool(pool_id: str, new_owner_id: str = None):
"""从公海池认领客户"""
dbp = DBP()
db = DBPools()
async with db.sqlorContext('default') as sor:
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:
pool_records = await sor.R("customer_pool", {"filters": [{"field": "id", "op": "=", "value": pool_id}]})
if not pool_records or len(pool_records) == 0:
raise ValueError("公海记录不存在")
pool_record = pool_records[0]
if pool_record["pool_status"] != "available":
raise ValueError("该客户已被认领或分配")
# 更新公海记录
await dbp.update(
await sor.U(
"customer_pool",
{
"assigned_to": new_owner_id,
@ -362,11 +378,11 @@ async def claim_from_pool(pool_id: str, new_owner_id: str = None):
"pool_status": "claimed",
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{"id": pool_id}
{"filters": [{"field": "id", "op": "=", "value": pool_id}]}
)
# 更新客户状态和负责人
await dbp.update(
await sor.U(
"customers",
{
"status": "active",
@ -374,7 +390,7 @@ async def claim_from_pool(pool_id: str, new_owner_id: str = None):
"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"]}
{"filters": [{"field": "id", "op": "=", "value": pool_record["customer_id"]}]}
)
return {"customer_id": pool_record["customer_id"], "new_owner_id": new_owner_id}
@ -382,36 +398,38 @@ async def claim_from_pool(pool_id: str, new_owner_id: str = None):
async def get_customer_360_view(customer_id: str) -> Dict:
"""获取客户360度视图"""
dbp = DBP()
db = DBPools()
async with db.sqlorContext('default') as sor:
# 客户基本信息
customer = await dbp.select_one("customers", {"id": customer_id})
if not customer:
customer_records = await sor.R("customers", {"filters": [{"field": "id", "op": "=", "value": customer_id}]})
if not customer_records or len(customer_records) == 0:
raise ValueError("客户不存在")
customer = customer_records[0]
# 商机记录
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"]}
)
opportunities = await sor.R("opportunities", {
"filters": [{"field": "customer_name", "op": "=", "value": customer["customer_name"]}],
"sortby": [{"field": "created_at", "direction": "desc"}]
})
# 合同历史(假设合同管理模块存在)
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}
)
contracts = await sor.R("contracts", {
"filters": [{"field": "customer_id", "op": "=", "value": customer_id}],
"sortby": [{"field": "created_at", "direction": "desc"}]
})
# 服务工单(假设服务模块存在)
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}
)
service_tickets = await sor.R("service_tickets", {
"filters": [{"field": "customer_id", "op": "=", "value": customer_id}],
"sortby": [{"field": "created_at", "direction": "desc"}]
})
# 回款情况
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}
)
payments = await sor.R("payment_records", {
"filters": [{"field": "customer_id", "op": "=", "value": customer_id}],
"sortby": [{"field": "due_date", "direction": "desc"}]
})
return {
"customer": customer,

View File

@ -0,0 +1,19 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
result = {'keys': [], 'rows': []}
try:
dbname = get_module_dbname('customer_management')
async with DBPools().sqlorContext(dbname) as sor:
ns = {'page': 1, 'rows': 50, 'sort': 'TABLE_NAME'}
sql = "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA='crm_db'"
rows = await sor.sqlExe(sql, ns)
if isinstance(rows, dict):
rows = rows.get('rows', [])
if rows:
result['keys'] = list(dict(rows[0]).keys())
result['rows'] = [list(dict(r).values()) for r in rows]
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Customer pool list API"""
import json
result = {'success': False, 'rows': [], 'total': 0}
try:
dbname = get_module_dbname('customer_management')
ns = {
'page': int(params_kw.get('page', 1)),
'rows': int(params_kw.get('rows', 20)),
'sort': 'created_at desc'
}
sql = "SELECT id, customer_id, original_owner_id, recycle_reason, inactive_days, recycled_at, pool_status, created_at FROM customer_pool"
async with DBPools().sqlorContext(dbname) as sor:
data = await sor.sqlExe(sql, ns)
if isinstance(data, dict):
result['total'] = data.get('total', 0)
result['rows'] = [dict(r) for r in data.get('rows', [])]
else:
result['rows'] = [dict(r) for r in (data or [])]
result['total'] = len(result['rows'])
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Customer create API"""
import json, uuid, time
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
try:
customer_name = params_kw.get('customer_name', '').strip()
customer_type = params_kw.get('customer_type', '').strip()
owner_id = params_kw.get('owner_id', '').strip()
if not customer_name:
result['options'] = {'title': 'Error', 'message': '客户名称不能为空', 'type': 'error'}
elif not customer_type:
result['options'] = {'title': 'Error', 'message': '客户类型不能为空', 'type': 'error'}
elif not owner_id:
result['options'] = {'title': 'Error', 'message': '负责人不能为空', 'type': 'error'}
else:
dbname = get_module_dbname('customer_management')
new_id = str(uuid.uuid4()).replace('-', '')
now = time.strftime('%Y-%m-%d %H:%M:%S')
phone = params_kw.get('phone', '').strip()
email = params_kw.get('email', '').strip()
tax_id = params_kw.get('tax_id', '').strip()
industry = params_kw.get('industry', '').strip()
customer_level = params_kw.get('customer_level', 'potential').strip()
address = params_kw.get('address', '').strip()
region = params_kw.get('region', '').strip()
status = params_kw.get('status', 'active').strip()
async with DBPools().sqlorContext(dbname) as sor:
await sor.sqlExe("""INSERT INTO customers (id, customer_name, customer_type, phone, email, tax_id, industry, customer_level, address, owner_id, region, status, created_at, updated_at)
VALUES (${id}$, ${customer_name}$, ${customer_type}$, ${phone}$, ${email}$, ${tax_id}$, ${industry}$, ${customer_level}$, ${address}$, ${owner_id}$, ${region}$, ${status}$, ${created_at}$, ${updated_at}$)""", {
'id': new_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,
'region': region, 'status': status,
'created_at': now, 'updated_at': now
})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '客户创建成功', 'type': 'success'}}
except Exception as e:
result['options'] = {'title': 'Error', 'message': '创建失败: ' + str(e), 'type': 'error'}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Customer delete API"""
import json
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
try:
record_id = params_kw.get('id', '').strip()
if not record_id:
result['options'] = {'title': 'Error', 'message': '记录ID不能为空', 'type': 'error'}
else:
dbname = get_module_dbname('customer_management')
async with DBPools().sqlorContext(dbname) as sor:
existing = await sor.sqlExe("SELECT id FROM customers WHERE id = ${id}$", {'id': record_id})
if not existing:
result['options'] = {'title': 'Error', 'message': '客户不存在', 'type': 'error'}
else:
await sor.sqlExe("DELETE FROM customers WHERE id = ${id}$", {'id': record_id})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '删除成功', 'type': 'success'}}
except Exception as e:
result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Customer list API - uses sqlor built-in pagination"""
import json
result = {'success': False, 'rows': [], 'total': 0}
try:
dbname = get_module_dbname('customer_management')
ns = {
'page': int(params_kw.get('page', 1)),
'rows': int(params_kw.get('rows', 20)),
'sort': 'created_at desc'
}
sql = "SELECT id, customer_name, customer_type, phone, email, industry, customer_level, region, status, created_at FROM customers"
async with DBPools().sqlorContext(dbname) as sor:
data = await sor.sqlExe(sql, ns)
if isinstance(data, dict):
result['total'] = data.get('total', 0)
result['rows'] = [dict(r) for r in data.get('rows', [])]
else:
result['rows'] = [dict(r) for r in (data or [])]
result['total'] = len(result['rows'])
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Customer update API"""
import json, time
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
try:
record_id = params_kw.get('id', '').strip()
if not record_id:
result['options'] = {'title': 'Error', 'message': '记录ID不能为空', 'type': 'error'}
else:
dbname = get_module_dbname('customer_management')
now = time.strftime('%Y-%m-%d %H:%M:%S')
fields = ['customer_name', 'customer_type', 'phone', 'email', 'tax_id', 'industry', 'customer_level', 'address', 'owner_id', 'region', 'status']
set_parts = []
params = {'id': record_id}
for f in fields:
val = params_kw.get(f, None)
if val is not None:
set_parts.append(f + " = ${" + f + "}$")
params[f] = val
set_parts.append("updated_at = ${updated_at}$")
params['updated_at'] = now
if len(set_parts) <= 1:
result['options'] = {'title': 'Error', 'message': '没有可更新的字段', 'type': 'error'}
else:
sql = "UPDATE customers SET " + ", ".join(set_parts) + " WHERE id = ${id}$"
async with DBPools().sqlorContext(dbname) as sor:
await sor.sqlExe(sql, params)
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '更新成功', 'type': 'success'}}
except Exception as e:
result['options'] = {'title': 'Error', 'message': '更新失败: ' + str(e), 'type': 'error'}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Customer handover list API"""
import json
result = {'success': False, 'rows': [], 'total': 0}
try:
dbname = get_module_dbname('customer_management')
ns = {
'page': int(params_kw.get('page', 1)),
'rows': int(params_kw.get('rows', 20)),
'sort': 'created_at desc'
}
sql = "SELECT id, customer_id, from_owner_id, to_owner_id, current_stage, handover_reason, created_at FROM customer_handover"
async with DBPools().sqlorContext(dbname) as sor:
data = await sor.sqlExe(sql, ns)
if isinstance(data, dict):
result['total'] = data.get('total', 0)
result['rows'] = [dict(r) for r in data.get('rows', [])]
else:
result['rows'] = [dict(r) for r in (data or [])]
result['total'] = len(result['rows'])
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -1,51 +1,67 @@
{
"widgettype": "TabPanel",
"options": {
"title": "客户管理"
},
"subwidgets": [
"type": "Page",
"title": "Customer Management",
"content": {
"type": "VBox",
"style": {"padding": "16px", "height": "100%"},
"children": [
{
"widgettype": "CRUD",
"options": {
"title": "客户档案",
"url": "{{entire_url(customers_list)}}"
}
"type": "HBox",
"justify": "space-between",
"style": {"marginBottom": "16px"},
"children": [
{"type": "Text", "content": "Customer List", "style": {"fontSize": "20px", "fontWeight": "bold"}},
{"type": "Button", "id": "addCustomerBtn", "text": "Add Customer", "variant": "primary", "leadingIcon": "add", "onclick": "openDialog('addCustomerDialog')"}
]
},
{
"widgettype": "CRUD",
"options": {
"title": "客户交接",
"url": "{{entire_url(handover_list)}}"
}
"type": "CRUD",
"id": "customerCRUD",
"api": {
"list": "main/customer_management/api/customers_list.dspy",
"create": "main/customer_management/api/customers_create.dspy",
"update": "main/customer_management/api/customers_update.dspy",
"delete": "main/customer_management/api/customers_delete.dspy"
},
{
"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"
}
"columns": [
{"field": "customer_name", "title": "Customer Name", "width": 180},
{"field": "customer_type", "title": "Type", "width": 100, "formatter": "code:customer_type"},
{"field": "phone", "title": "Phone", "width": 130},
{"field": "email", "title": "Email", "width": 180},
{"field": "industry", "title": "Industry", "width": 120},
{"field": "customer_level", "title": "Level", "width": 100, "formatter": "code:customer_level"},
{"field": "region", "title": "Region", "width": 100},
{"field": "status", "title": "Status", "width": 80, "formatter": "code:customer_status"},
{"field": "created_at", "title": "Created", "width": 160}
],
"onSubmit": "get_customer_360_view"
"style": {"flex": 1}
},
{
"type": "Dialog",
"id": "addCustomerDialog",
"title": "Add Customer",
"width": 600,
"content": {
"type": "Form",
"id": "addCustomerForm",
"fields": [
{"field": "customer_name", "title": "Customer Name", "uitype": "TextField", "required": true},
{"field": "customer_type", "title": "Type", "uitype": "SelectField", "required": true, "options": "code:customer_type"},
{"field": "phone", "title": "Phone", "uitype": "TextField"},
{"field": "email", "title": "Email", "uitype": "TextField"},
{"field": "tax_id", "title": "Tax ID", "uitype": "TextField"},
{"field": "industry", "title": "Industry", "uitype": "TextField"},
{"field": "customer_level", "title": "Level", "uitype": "SelectField", "options": "code:customer_level", "value": "potential"},
{"field": "address", "title": "Address", "uitype": "TextField"},
{"field": "region", "title": "Region", "uitype": "TextField"},
{"field": "owner_id", "title": "Owner ID", "uitype": "TextField", "required": true}
],
"actions": [
{"type": "Button", "text": "Cancel", "onclick": "closeDialog('addCustomerDialog')"},
{"type": "Button", "text": "Submit", "variant": "primary", "onclick": "submitForm('addCustomerForm', 'main/customer_management/api/customers_create.dspy', 'addCustomerDialog', 'customerCRUD')"}
]
}
}
]
}
]
}

65
wwwroot/customer_edit.ui Normal file
View File

@ -0,0 +1,65 @@
{
"widgettype": "Page",
"options": {
"title": "客户编辑",
"style": {"height": "100vh", "padding": "0"}
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"style": {"padding": "16px", "flex": 1, "overflow": "auto"}},
"subwidgets": [
{
"widgettype": "Form",
"id": "customer_form",
"options": {
"submit_url": "{{entire_url('api/customers_create.dspy')}}",
"method": "POST",
"layout": "vertical",
"style": {"maxWidth": "600px"},
"fields": [
{"name": "customer_name", "label": "客户名称", "uitype": "text", "required": true},
{"name": "customer_type", "label": "客户类型", "uitype": "code", "required": true, "data": [
{"value": "enterprise", "text": "企业客户"},
{"value": "individual", "text": "个人客户"},
{"value": "government", "text": "政府客户"}
]},
{"name": "contact_person", "label": "联系人", "uitype": "text"},
{"name": "phone", "label": "电话", "uitype": "text"},
{"name": "email", "label": "邮箱", "uitype": "text"},
{"name": "tax_id", "label": "税号", "uitype": "text"},
{"name": "industry", "label": "行业", "uitype": "code", "data": [
{"value": "manufacturing", "text": "制造业"},
{"value": "technology", "text": "科技"},
{"value": "finance", "text": "金融"},
{"value": "retail", "text": "零售"},
{"value": "other", "text": "其他"}
]},
{"name": "customer_level", "label": "客户等级", "uitype": "code", "data": [
{"value": "potential", "text": "潜在客户"},
{"value": "active", "text": "活跃客户"},
{"value": "vip", "text": "VIP客户"}
]},
{"name": "address", "label": "地址", "uitype": "textarea"},
{"name": "owner_id", "label": "负责人", "uitype": "text", "required": true},
{"name": "region", "label": "区域", "uitype": "code", "data": [
{"value": "east", "text": "华东"},
{"value": "south", "text": "华南"},
{"value": "west", "text": "华西"},
{"value": "north", "text": "华北"}
]},
{"name": "status", "label": "状态", "uitype": "code", "data": [
{"value": "active", "text": "有效"},
{"value": "inactive", "text": "无效"}
]}
],
"buttons": [
{"type": "submit", "text": "保存", "variant": "primary"},
{"type": "button", "text": "取消", "action": "navigate('main/customer_management/customer_list.ui')"}
]
}
}
]
}
]
}

45
wwwroot/customer_list.ui Normal file
View File

@ -0,0 +1,45 @@
{
"widgettype": "Page",
"options": {
"title": "客户列表",
"style": {"height": "100vh", "padding": "0"}
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"style": {"padding": "16px", "flex": 1, "overflow": "hidden"}},
"subwidgets": [
{
"widgettype": "HBox",
"options": {"style": {"marginBottom": "16px", "gap": "8px"}},
"subwidgets": [
{"widgettype": "TextField", "id": "search_keyword", "options": {"label": "搜索", "placeholder": "客户名称/编码", "style": {"flex": 1}}},
{"widgettype": "Button", "id": "btn_search", "options": {"text": "搜索", "variant": "primary"}}
]
},
{
"widgettype": "DataGrid",
"id": "customer_grid",
"options": {
"url": "{{entire_url('api/customers_list.dspy')}}",
"style": {"flex": 1},
"columns": [
{"field": "customer_code", "header": "客户编码", "width": 120},
{"field": "customer_name", "header": "客户名称", "width": 200},
{"field": "customer_type", "header": "类型", "width": 100},
{"field": "industry", "header": "行业", "width": 120},
{"field": "contact_person", "header": "联系人", "width": 100},
{"field": "phone", "header": "电话", "width": 120},
{"field": "status", "header": "状态", "width": 80}
],
"toolbar": [
{"type": "button", "text": "新增", "icon": "add", "action": "navigate('main/customer_management/customer_edit.ui')"},
{"type": "button", "text": "编辑", "icon": "edit", "action": "navigate('main/customer_management/customer_edit.ui?id={% raw %}{{selectedRow.id}}{% endraw %}')"},
{"type": "button", "text": "删除", "icon": "delete", "action": "doDelete('{% raw %}{{selectedRow.id}}{% endraw %}')"}
]
}
}
]
}
]
}

34
wwwroot/customer_pool.ui Normal file
View File

@ -0,0 +1,34 @@
{
"widgettype": "Page",
"options": {
"title": "客户公海",
"style": {"height": "100vh", "padding": "0"}
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"style": {"padding": "16px", "flex": 1, "overflow": "hidden"}},
"subwidgets": [
{
"widgettype": "DataGrid",
"id": "pool_grid",
"options": {
"url": "{{entire_url('api/customer_pool_list.dspy')}}",
"style": {"flex": 1},
"columns": [
{"field": "customer_code", "header": "客户编码", "width": 120},
{"field": "customer_name", "header": "客户名称", "width": 200},
{"field": "industry", "header": "行业", "width": 120},
{"field": "recycle_reason", "header": "回收原因", "width": 150},
{"field": "pool_status", "header": "状态", "width": 80},
{"field": "recycle_time", "header": "回收时间", "width": 150}
],
"toolbar": [
{"type": "button", "text": "领取", "icon": "get_app", "action": "claimCustomer('{% raw %}{{selectedRow.id}}{% endraw %}')"}
]
}
}
]
}
]
}

31
wwwroot/handover_list.ui Normal file
View File

@ -0,0 +1,31 @@
{
"widgettype": "Page",
"options": {
"title": "客户交接",
"style": {"height": "100vh", "padding": "0"}
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"style": {"padding": "16px", "flex": 1, "overflow": "hidden"}},
"subwidgets": [
{
"widgettype": "DataGrid",
"id": "handover_grid",
"options": {
"url": "{{entire_url('api/handover_list.dspy')}}",
"style": {"flex": 1},
"columns": [
{"field": "customer_name", "header": "客户名称", "width": 200},
{"field": "from_owner_name", "header": "原负责人", "width": 120},
{"field": "to_owner_name", "header": "新负责人", "width": 120},
{"field": "handover_reason", "header": "交接原因", "width": 200},
{"field": "status", "header": "状态", "width": 80},
{"field": "created_at", "header": "创建时间", "width": 150}
]
}
}
]
}
]
}