diff --git a/customer_management/core.py b/customer_management/core.py index 7403ad4..92a6abe 100644 --- a/customer_management/core.py +++ b/customer_management/core.py @@ -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,47 +21,48 @@ async def create_customer( 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 + db = DBPools() + async with db.sqlorContext('default') as sor: + customer_id = str(uuid.uuid4()).replace('-', '') + + # 数据校验 + await validate_customer_data(sor, 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 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,39 +73,40 @@ async def initiate_handover( 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 + 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"] != "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 sor.C("customer_handover", handover_data) + + # 自动生成交接清单 + 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,233 +198,246 @@ 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: - 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"} + 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 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") + }, + {"filters": [{"field": "id", "op": "=", "value": 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"} + db = DBPools() + async with db.sqlorContext('default') as sor: + approver_id = approver_id or get_current_user_id() + + 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 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") + }, + {"filters": [{"field": "id", "op": "=", "value": 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"]} + db = DBPools() + async with db.sqlorContext('default') as sor: + confirm_by = confirm_by or get_current_user_id() + + 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 sor.U( + "customers", + { + "owner_id": handover["to_owner_id"], + "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + }, + {"filters": [{"field": "id", "op": "=", "value": handover["customer_id"]}]} + ) + + # 完成交接流程 + await sor.U( + "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") + }, + {"filters": [{"field": "id", "op": "=", "value": handover_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 - notification_content = f"尊敬的{customer['customer_name']},您的客户经理已变更为{new_owner.get('name', '新经理')},联系方式:{new_owner.get('phone', '待更新')}。" + 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": "待更新"} - # 记录通知日志(实际应调用短信/邮件API) - print(f"客户通知已发送: {notification_content}") + if customer: + 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 + 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("客户已在公海池中") + + # 创建公海记录 + 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 sor.C("customer_pool", pool_data) + + # 更新客户状态 + await sor.U( + "customers", + { + "status": "in_pool", + "owner_id": "", # 清空负责人 + "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + }, + {"filters": [{"field": "id", "op": "=", "value": 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} + db = DBPools() + async with db.sqlorContext('default') as sor: + new_owner_id = new_owner_id or get_current_user_id() + + 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 sor.U( + "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") + }, + {"filters": [{"field": "id", "op": "=", "value": pool_id}]} + ) + + # 更新客户状态和负责人 + await sor.U( + "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") + }, + {"filters": [{"field": "id", "op": "=", "value": 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 - } + 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] + + # 商机记录 + opportunities = await sor.R("opportunities", { + "filters": [{"field": "customer_name", "op": "=", "value": customer["customer_name"]}], + "sortby": [{"field": "created_at", "direction": "desc"}] + }) + + # 合同历史(假设合同管理模块存在) + contracts = await sor.R("contracts", { + "filters": [{"field": "customer_id", "op": "=", "value": customer_id}], + "sortby": [{"field": "created_at", "direction": "desc"}] + }) + + # 服务工单(假设服务模块存在) + service_tickets = await sor.R("service_tickets", { + "filters": [{"field": "customer_id", "op": "=", "value": customer_id}], + "sortby": [{"field": "created_at", "direction": "desc"}] + }) + + # 回款情况 + payments = await sor.R("payment_records", { + "filters": [{"field": "customer_id", "op": "=", "value": customer_id}], + "sortby": [{"field": "due_date", "direction": "desc"}] + }) + + return { + "customer": customer, + "opportunities": opportunities, + "contracts": contracts, + "service_tickets": service_tickets, + "payments": payments + } def get_current_user_id() -> str: diff --git a/wwwroot/api/check_tables.dspy b/wwwroot/api/check_tables.dspy new file mode 100644 index 0000000..16a4b24 --- /dev/null +++ b/wwwroot/api/check_tables.dspy @@ -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) diff --git a/wwwroot/api/customer_pool_list.dspy b/wwwroot/api/customer_pool_list.dspy new file mode 100644 index 0000000..96f0358 --- /dev/null +++ b/wwwroot/api/customer_pool_list.dspy @@ -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) diff --git a/wwwroot/api/customers_create.dspy b/wwwroot/api/customers_create.dspy new file mode 100644 index 0000000..72c24da --- /dev/null +++ b/wwwroot/api/customers_create.dspy @@ -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) diff --git a/wwwroot/api/customers_delete.dspy b/wwwroot/api/customers_delete.dspy new file mode 100644 index 0000000..f9c672f --- /dev/null +++ b/wwwroot/api/customers_delete.dspy @@ -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) diff --git a/wwwroot/api/customers_list.dspy b/wwwroot/api/customers_list.dspy new file mode 100644 index 0000000..bff4193 --- /dev/null +++ b/wwwroot/api/customers_list.dspy @@ -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) diff --git a/wwwroot/api/customers_update.dspy b/wwwroot/api/customers_update.dspy new file mode 100644 index 0000000..ada74e8 --- /dev/null +++ b/wwwroot/api/customers_update.dspy @@ -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) diff --git a/wwwroot/api/handover_list.dspy b/wwwroot/api/handover_list.dspy new file mode 100644 index 0000000..3e3cb1a --- /dev/null +++ b/wwwroot/api/handover_list.dspy @@ -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) diff --git a/wwwroot/base.ui b/wwwroot/base.ui index 1f801ca..b6a6ec8 100644 --- a/wwwroot/base.ui +++ b/wwwroot/base.ui @@ -1,51 +1,67 @@ { - "widgettype": "TabPanel", - "options": { - "title": "客户管理" - }, - "subwidgets": [ - { - "widgettype": "CRUD", - "options": { - "title": "客户档案", - "url": "{{entire_url(customers_list)}}" - } + "type": "Page", + "title": "Customer Management", + "content": { + "type": "VBox", + "style": {"padding": "16px", "height": "100%"}, + "children": [ + { + "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')"} + ] + }, + { + "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(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" - } - } - ] + "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} + ], + "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')"} + ] } + } ] + } } \ No newline at end of file diff --git a/wwwroot/customer_edit.ui b/wwwroot/customer_edit.ui new file mode 100644 index 0000000..5a06d9d --- /dev/null +++ b/wwwroot/customer_edit.ui @@ -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')"} + ] + } + } + ] + } + ] +} diff --git a/wwwroot/customer_list.ui b/wwwroot/customer_list.ui new file mode 100644 index 0000000..a8be893 --- /dev/null +++ b/wwwroot/customer_list.ui @@ -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 %}')"} + ] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/wwwroot/customer_pool.ui b/wwwroot/customer_pool.ui new file mode 100644 index 0000000..4367bdc --- /dev/null +++ b/wwwroot/customer_pool.ui @@ -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 %}')"} + ] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/wwwroot/handover_list.ui b/wwwroot/handover_list.ui new file mode 100644 index 0000000..a32cae1 --- /dev/null +++ b/wwwroot/handover_list.ui @@ -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} + ] + } + } + ] + } + ] +} \ No newline at end of file