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
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
"""
|
||||
订单管理 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)
|
||||
Reference in New Issue
Block a user