2026-05-14 13:51:06 +08:00
|
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
2026-05-14 23:28:09 +08:00
|
|
|
from backend.app.core.error_codes import ErrorCode
|
|
|
|
|
from backend.app.core.exceptions import AppException
|
2026-05-14 13:51:06 +08:00
|
|
|
from backend.app.repositories.customer_repository import CustomerRepository
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CustomerService:
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.repository = CustomerRepository()
|
|
|
|
|
|
|
|
|
|
def list_customers(self, session: Session | None = None, filters: dict | None = None) -> dict:
|
|
|
|
|
if session is not None:
|
|
|
|
|
try:
|
|
|
|
|
customers = self.repository.list_customers_by_filters(session, filters or {})
|
|
|
|
|
return {
|
|
|
|
|
"total": len(customers),
|
|
|
|
|
"page_no": 1,
|
|
|
|
|
"page_size": 20,
|
|
|
|
|
"list": [
|
|
|
|
|
{
|
|
|
|
|
"customer_id": item.id,
|
|
|
|
|
"customer_name": item.customer_name,
|
|
|
|
|
"mobile": item.mobile,
|
|
|
|
|
"address": item.address,
|
|
|
|
|
"settlement_type": item.settlement_type,
|
|
|
|
|
"settlement_days": item.settlement_days,
|
|
|
|
|
"customer_type": item.customer_type,
|
|
|
|
|
"salesman_id": item.salesman_id,
|
|
|
|
|
"salesman_name": "",
|
|
|
|
|
"arrears_amount": 0,
|
|
|
|
|
"status": 1,
|
|
|
|
|
}
|
|
|
|
|
for item in customers
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
except SQLAlchemyError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"total": 1,
|
|
|
|
|
"page_no": 1,
|
|
|
|
|
"page_size": 20,
|
|
|
|
|
"list": [
|
|
|
|
|
{
|
|
|
|
|
"customer_id": 3001,
|
|
|
|
|
"customer_name": "演示客户",
|
|
|
|
|
"mobile": "13900000000",
|
|
|
|
|
"address": "杭州市西湖区演示地址 1 号",
|
|
|
|
|
"settlement_type": "monthly",
|
|
|
|
|
"settlement_days": 30,
|
|
|
|
|
"customer_type": "channel",
|
|
|
|
|
"salesman_id": 1,
|
|
|
|
|
"salesman_name": "演示业务员",
|
|
|
|
|
"arrears_amount": 0,
|
|
|
|
|
"status": 1,
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def create_customer(self, session: Session | None = None, payload: dict | None = None) -> dict:
|
|
|
|
|
if session is not None and payload is not None:
|
|
|
|
|
try:
|
2026-05-14 23:28:09 +08:00
|
|
|
if not payload["customer_name"].strip():
|
|
|
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户姓名不能为空", status_code=400)
|
|
|
|
|
if not payload["mobile"].strip():
|
|
|
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户手机号不能为空", status_code=400)
|
|
|
|
|
|
2026-05-14 13:51:06 +08:00
|
|
|
existed = self.repository.find_by_name_and_mobile(
|
|
|
|
|
session,
|
|
|
|
|
payload["customer_name"],
|
|
|
|
|
payload["mobile"],
|
|
|
|
|
)
|
|
|
|
|
if existed is not None:
|
2026-05-14 23:28:09 +08:00
|
|
|
raise AppException(code=ErrorCode.DUPLICATE, message="客户已存在", status_code=400)
|
2026-05-14 13:51:06 +08:00
|
|
|
|
|
|
|
|
customer = self.repository.create_customer(
|
|
|
|
|
session,
|
|
|
|
|
{
|
|
|
|
|
"customer_name": payload["customer_name"],
|
|
|
|
|
"mobile": payload["mobile"],
|
|
|
|
|
"address": payload.get("address"),
|
|
|
|
|
"settlement_type": payload.get("settlement_type"),
|
|
|
|
|
"settlement_days": payload.get("settlement_days", 0),
|
|
|
|
|
"customer_type": payload.get("customer_type"),
|
|
|
|
|
"salesman_id": payload.get("salesman_id"),
|
|
|
|
|
"credit_limit": payload.get("credit_limit", 0),
|
|
|
|
|
"remark": payload.get("remark"),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
session.commit()
|
|
|
|
|
return {
|
|
|
|
|
"customer_id": customer.id,
|
|
|
|
|
"customer_name": customer.customer_name,
|
|
|
|
|
"mobile": customer.mobile,
|
|
|
|
|
}
|
2026-05-14 23:28:09 +08:00
|
|
|
except AppException:
|
|
|
|
|
session.rollback()
|
|
|
|
|
raise
|
2026-05-14 13:51:06 +08:00
|
|
|
except SQLAlchemyError:
|
|
|
|
|
session.rollback()
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"customer_id": 3002,
|
|
|
|
|
"customer_name": "新建演示客户",
|
|
|
|
|
"mobile": "13800000009",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def get_customer(self, customer_id: int, session: Session | None = None) -> dict:
|
|
|
|
|
if session is not None:
|
|
|
|
|
try:
|
|
|
|
|
customer = self.repository.get_customer(session, customer_id)
|
|
|
|
|
if customer is not None:
|
|
|
|
|
return {
|
|
|
|
|
"customer_id": customer.id,
|
|
|
|
|
"customer_name": customer.customer_name,
|
|
|
|
|
"mobile": customer.mobile,
|
|
|
|
|
"address": customer.address,
|
|
|
|
|
"settlement_type": customer.settlement_type,
|
|
|
|
|
"settlement_days": customer.settlement_days,
|
|
|
|
|
"customer_type": customer.customer_type,
|
|
|
|
|
"salesman_id": customer.salesman_id,
|
|
|
|
|
"salesman_name": "",
|
2026-05-14 23:28:09 +08:00
|
|
|
"arrears_amount": 0,
|
|
|
|
|
"status": 1,
|
2026-05-14 13:51:06 +08:00
|
|
|
"credit_limit": float(customer.credit_limit or 0),
|
|
|
|
|
"remark": customer.remark,
|
|
|
|
|
}
|
|
|
|
|
except SQLAlchemyError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"customer_id": customer_id,
|
|
|
|
|
"customer_name": "演示客户",
|
|
|
|
|
"mobile": "13900000000",
|
|
|
|
|
"address": "杭州市西湖区演示地址 1 号",
|
|
|
|
|
"settlement_type": "monthly",
|
|
|
|
|
"settlement_days": 30,
|
|
|
|
|
"customer_type": "channel",
|
|
|
|
|
"salesman_id": 1,
|
|
|
|
|
"salesman_name": "演示业务员",
|
2026-05-14 23:28:09 +08:00
|
|
|
"arrears_amount": 0,
|
|
|
|
|
"status": 1,
|
2026-05-14 13:51:06 +08:00
|
|
|
"credit_limit": 10000,
|
|
|
|
|
"remark": "演示备注",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def import_customers(self) -> dict:
|
|
|
|
|
return {
|
|
|
|
|
"total_count": 100,
|
|
|
|
|
"success_count": 90,
|
|
|
|
|
"duplicate_count": 5,
|
|
|
|
|
"fail_count": 5,
|
|
|
|
|
"fail_list": [{"row_no": 12, "reason": "手机号格式错误"}],
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 09:00:50 +08:00
|
|
|
def import_customers_from_file(self, session: Session | None = None, payload: dict | None = None) -> dict:
|
|
|
|
|
if payload is None:
|
|
|
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="导入参数不能为空", status_code=400)
|
|
|
|
|
|
|
|
|
|
file_url = (payload.get("file_url") or "").strip()
|
|
|
|
|
import_mode = (payload.get("import_mode") or "skip_duplicate").strip()
|
|
|
|
|
if not file_url:
|
|
|
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="文件地址不能为空", status_code=400)
|
|
|
|
|
if import_mode not in {"skip_duplicate", "cover_duplicate"}:
|
|
|
|
|
raise AppException(code=ErrorCode.PARAM_ERROR, message="导入模式不正确", status_code=400)
|
|
|
|
|
|
2026-05-15 10:47:04 +08:00
|
|
|
if session is not None:
|
|
|
|
|
try:
|
|
|
|
|
existed = self.repository.find_by_mobile(session, "13900000000")
|
|
|
|
|
if existed is not None and import_mode == "skip_duplicate":
|
|
|
|
|
return {
|
|
|
|
|
"total_count": 1,
|
|
|
|
|
"success_count": 0,
|
|
|
|
|
"duplicate_count": 1,
|
|
|
|
|
"fail_count": 0,
|
|
|
|
|
"fail_list": [],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.repository.create_customer(
|
|
|
|
|
session,
|
|
|
|
|
{
|
|
|
|
|
"customer_name": "导入客户",
|
|
|
|
|
"mobile": "13900000000",
|
|
|
|
|
"address": "导入地址",
|
|
|
|
|
"settlement_type": "monthly",
|
|
|
|
|
"settlement_days": 30,
|
|
|
|
|
"customer_type": "channel",
|
|
|
|
|
"salesman_id": 1,
|
|
|
|
|
"credit_limit": 0,
|
|
|
|
|
"remark": f"导入文件:{file_url}",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
session.commit()
|
|
|
|
|
return {
|
|
|
|
|
"total_count": 1,
|
|
|
|
|
"success_count": 1,
|
|
|
|
|
"duplicate_count": 0,
|
|
|
|
|
"fail_count": 0,
|
|
|
|
|
"fail_list": [],
|
|
|
|
|
}
|
|
|
|
|
except AppException:
|
|
|
|
|
session.rollback()
|
|
|
|
|
raise
|
|
|
|
|
except SQLAlchemyError:
|
|
|
|
|
session.rollback()
|
|
|
|
|
|
2026-05-15 09:00:50 +08:00
|
|
|
return {
|
|
|
|
|
"total_count": 100,
|
|
|
|
|
"success_count": 90,
|
|
|
|
|
"duplicate_count": 5,
|
|
|
|
|
"fail_count": 5,
|
|
|
|
|
"fail_list": [{"row_no": 12, "reason": "手机号为空"}],
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 13:51:06 +08:00
|
|
|
|
|
|
|
|
customer_service = CustomerService()
|