Files
crm_project/server/app/services/order_service.py
T
hankin 423baff73b v0.1.0: CRM/ERP 系统内测版本 - 安全加固完成
- Docker bridge 网络隔离(8000 端口封死)
- Gunicorn 4 Worker 多进程
- Alembic 数据库迁移基线
- 日志轮转 20m×3
- JWT 密钥 + DB 密码 + CORS 收紧
- 3-2-1 备份链路(NAS + R740-B 冷备)
- 连接池 pool_pre_ping + pool_recycle=3600
2026-03-16 07:31:37 +00:00

301 lines
9.2 KiB
Python

"""
订单管理 Service 层
REST API 路由 和 MCP 工具 共用此层函数
"""
from __future__ import annotations
import uuid
from datetime import date, datetime
from typing import Any
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.exceptions import BizException, ForbiddenException, NotFoundException
from app.models.crm import CrmCustomer
from app.models.erp import ProductSku
from app.models.order import ErpOrder, ErpOrderItem
from app.schemas.auth import CurrentUserPayload
from app.schemas.order import (
OrderBriefResponse,
OrderCreate,
OrderItemResponse,
OrderListResponse,
OrderResponse,
PriceCalculateResponse,
)
# ── 工具函数 ─────────────────────────────────────────────
async def _generate_order_no(db: AsyncSession) -> str:
today = date.today().strftime("%Y%m%d")
prefix = f"ORD-{today}-"
stmt = (
select(func.count())
.select_from(ErpOrder)
.where(ErpOrder.order_no.like(f"{prefix}%"))
)
count = (await db.execute(stmt)).scalar() or 0
return f"{prefix}{count + 1:03d}"
def _item_to_response(item: ErpOrderItem) -> OrderItemResponse:
return OrderItemResponse(
id=item.id,
sku_id=item.sku_id,
sku_code=item.sku.sku_code if item.sku else None,
sku_name=item.sku.name if item.sku else None,
spec=item.sku.spec if item.sku else None,
qty=float(item.qty),
unit_price=float(item.unit_price),
sub_total=float(item.sub_total),
shipped_qty=float(item.shipped_qty or 0),
)
def _order_to_response(o: ErpOrder, with_items: bool = True) -> OrderResponse:
return OrderResponse(
id=o.id,
order_no=o.order_no,
customer_id=o.customer_id,
customer_name=o.customer.name if o.customer else None,
salesperson_id=o.salesperson_id,
salesperson_name=o.salesperson.real_name if o.salesperson else None,
total_amount=float(o.total_amount or 0),
shipping_state=o.shipping_state,
payment_state=o.payment_state,
paid_amount=float(o.paid_amount or 0),
remark=o.remark,
order_date=o.order_date,
items=[_item_to_response(i) for i in o.items] if with_items else [],
created_at=o.created_at,
updated_at=o.updated_at,
)
def _order_to_brief(o: ErpOrder) -> OrderBriefResponse:
return OrderBriefResponse(
id=o.id,
order_no=o.order_no,
customer_id=o.customer_id,
customer_name=o.customer.name if o.customer else None,
salesperson_name=o.salesperson.real_name if o.salesperson else None,
total_amount=float(o.total_amount or 0),
shipping_state=o.shipping_state,
payment_state=o.payment_state,
paid_amount=float(o.paid_amount or 0),
order_date=o.order_date,
created_at=o.created_at,
)
def _check_order_access(order: ErpOrder, user: CurrentUserPayload) -> None:
if user.data_scope == "all":
return
if user.data_scope == "self":
if order.salesperson_id != user.user_id:
raise ForbiddenException("无权访问该订单(数据权限:仅本人)")
# ── Service Functions ────────────────────────────────────
async def calculate_price(
db: AsyncSession,
customer_id: uuid.UUID,
sku_id: uuid.UUID,
) -> PriceCalculateResponse:
sku = (
await db.execute(
select(ProductSku).where(
ProductSku.id == sku_id,
ProductSku.is_deleted.is_(False),
)
)
).scalar_one_or_none()
if sku is None:
raise NotFoundException("产品 SKU 不存在")
# 历史成交价追溯
history_stmt = (
select(ErpOrderItem.unit_price, ErpOrder.order_no, ErpOrder.order_date)
.join(ErpOrder, ErpOrderItem.order_id == ErpOrder.id)
.where(
ErpOrder.customer_id == customer_id,
ErpOrderItem.sku_id == sku_id,
ErpOrder.is_deleted.is_(False),
ErpOrderItem.is_deleted.is_(False),
)
.order_by(ErpOrder.created_at.desc())
.limit(1)
)
history = (await db.execute(history_stmt)).first()
if history:
return PriceCalculateResponse(
sku_id=sku.id,
sku_code=sku.sku_code,
sku_name=sku.name,
unit_price=float(history.unit_price),
price_source="history",
last_order_no=history.order_no,
last_order_date=history.order_date,
)
return PriceCalculateResponse(
sku_id=sku.id,
sku_code=sku.sku_code,
sku_name=sku.name,
unit_price=float(sku.standard_price or 0),
price_source="standard",
)
async def create_order(
db: AsyncSession,
user: CurrentUserPayload,
body: OrderCreate,
) -> OrderResponse:
# 校验客户存在
cust = (
await db.execute(
select(CrmCustomer).where(
CrmCustomer.id == body.customer_id,
CrmCustomer.is_deleted.is_(False),
)
)
).scalar_one_or_none()
if cust is None:
raise NotFoundException("客户不存在")
# 校验所有 SKU 存在
sku_ids = [item.sku_id for item in body.items]
skus = (
await db.execute(
select(ProductSku).where(
ProductSku.id.in_(sku_ids),
ProductSku.is_deleted.is_(False),
)
)
).scalars().all()
found_ids = {s.id for s in skus}
missing = [str(sid) for sid in sku_ids if sid not in found_ids]
if missing:
raise BizException(message=f"以下 SKU 不存在: {', '.join(missing)}")
try:
async with db.begin_nested():
order_no = await _generate_order_no(db)
total = sum(item.qty * item.unit_price for item in body.items)
order = ErpOrder(
order_no=order_no,
customer_id=body.customer_id,
salesperson_id=user.user_id,
total_amount=total,
shipping_state="pending",
payment_state="unpaid",
paid_amount=0,
remark=body.remark,
order_date=body.order_date or date.today(),
)
db.add(order)
await db.flush()
for item in body.items:
order_item = ErpOrderItem(
order_id=order.id,
sku_id=item.sku_id,
qty=item.qty,
unit_price=item.unit_price,
sub_total=round(item.qty * item.unit_price, 2),
shipped_qty=0,
)
db.add(order_item)
await db.commit()
except BizException:
raise
except Exception as e:
await db.rollback()
raise BizException(code=500, message=f"订单创建事务失败: {e!s}") from e
refreshed = (
await db.execute(select(ErpOrder).where(ErpOrder.id == order.id))
).scalar_one()
return _order_to_response(refreshed)
async def list_orders(
db: AsyncSession,
user: CurrentUserPayload,
page: int = 1,
size: int = 20,
customer_id: uuid.UUID | None = None,
shipping_state: str | None = None,
payment_state: str | None = None,
keyword: str | None = None,
) -> OrderListResponse:
where: list[Any] = [ErpOrder.is_deleted.is_(False)]
if user.data_scope == "self":
where.append(ErpOrder.salesperson_id == user.user_id)
elif user.data_scope == "dept_and_sub":
if user.dept_id is not None:
from app.models.sys import SysUser
sub = select(SysUser.id).where(
SysUser.dept_id == user.dept_id,
SysUser.is_deleted.is_(False),
)
where.append(ErpOrder.salesperson_id.in_(sub))
if customer_id:
where.append(ErpOrder.customer_id == customer_id)
if shipping_state:
where.append(ErpOrder.shipping_state == shipping_state)
if payment_state:
where.append(ErpOrder.payment_state == payment_state)
if keyword:
where.append(ErpOrder.order_no.ilike(f"%{keyword}%"))
total = (
await db.execute(select(func.count()).select_from(ErpOrder).where(*where))
).scalar() or 0
stmt = (
select(ErpOrder)
.where(*where)
.order_by(ErpOrder.created_at.desc())
.offset((page - 1) * size)
.limit(size)
)
orders = (await db.execute(stmt)).scalars().all()
return OrderListResponse(
total=total,
items=[_order_to_brief(o) for o in orders],
page=page,
size=size,
)
async def get_order(
db: AsyncSession,
user: CurrentUserPayload,
order_id: uuid.UUID,
) -> OrderResponse:
order = (
await db.execute(
select(ErpOrder).where(
ErpOrder.id == order_id,
ErpOrder.is_deleted.is_(False),
)
)
).scalar_one_or_none()
if order is None:
raise NotFoundException("订单不存在或已被删除")
_check_order_access(order, user)
return _order_to_response(order)