批次5:inventory服务下沉收口 + Pydantic v2 / datetime弃用清零

- inventory业务层下沉(薄路由+service orchestration模式):
  - customer/supplier/warehouse -> master_data_service
  - material_routes(价格历史/趋势/供应商关联)-> material_service
  - product_routes(CRUD/BOM/from-task跨模块桥接)-> product_service
  - dashboard_routes(首页统计/低库存预警)-> dashboard_service
- inventory侧新增service回归覆盖(dashboard 2 / master_data 10 /
  material 10 / product 14),含跨模块桥接测试种子
- Pydantic v2弃用清零:全仓14处 class Config 全部迁移到
  model_config = ConfigDict(from_attributes=True)(含 shared auth)
- datetime.utcnow() 弃用清零:auth_service 3处统一改 datetime.now(timezone.utc)
- 同步文档:STATUS / ROADMAP / TECH_DEBT(D12清偿)/ AGENTS 代码地图

测试基线:126 passed, 4 skipped(无deprecation warning)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-22 16:13:45 +08:00
parent c51e6b793a
commit 64dc85bd14
32 changed files with 1495 additions and 690 deletions
@@ -0,0 +1,71 @@
"""仪表盘聚合业务服务层
将 dashboard_routes 中的聚合查询与统计编排下沉到此,
路由层只做依赖注入与响应返回。
"""
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from inventory.models import Customer, Inventory, Product, PurchaseOrder, SalesOrder, Supplier, Warehouse
class DashboardService:
"""仪表盘统计服务"""
@staticmethod
async def get_dashboard(db_session: AsyncSession) -> dict:
material_count = await db_session.scalar(
select(func.count(Product.id)).where(Product.is_active == True, Product.item_type == "material")
) or 0
finished_product_count = await db_session.scalar(
select(func.count(Product.id)).where(Product.is_active == True, Product.item_type == "finished")
) or 0
supplier_count = await db_session.scalar(select(func.count(Supplier.id)).where(Supplier.is_active == True)) or 0
customer_count = await db_session.scalar(select(func.count(Customer.id)).where(Customer.is_active == True)) or 0
warehouse_count = await db_session.scalar(select(func.count(Warehouse.id)).where(Warehouse.is_active == True)) or 0
total_stock = await db_session.scalar(
select(func.sum(Inventory.quantity))
.join(Product, Inventory.product_id == Product.id)
.where(Product.item_type == "material")
) or 0
total_value = await db_session.scalar(
select(func.sum(Inventory.quantity * Product.cost_price))
.join(Product, Inventory.product_id == Product.id)
.where(Product.item_type == "material")
) or 0
pending_purchase = await db_session.scalar(
select(func.count(PurchaseOrder.id)).where(PurchaseOrder.status == "pending")
) or 0
pending_sales = await db_session.scalar(
select(func.count(SalesOrder.id)).where(SalesOrder.status == "pending")
) or 0
low_stock_products = await db_session.execute(
select(Product, Inventory)
.join(Inventory, Product.id == Inventory.product_id)
.where(Product.item_type == "material")
.where(Inventory.quantity <= Product.min_stock)
.limit(10)
)
low_stock = [
{"id": p.id, "name": p.name, "sku": p.sku, "quantity": i.quantity, "min_stock": p.min_stock}
for p, i in low_stock_products.all()
]
return {
"finished_product_count": finished_product_count,
"material_count": material_count,
"supplier_count": supplier_count,
"customer_count": customer_count,
"warehouse_count": warehouse_count,
"total_stock": total_stock,
"total_value": round(total_value, 2),
"pending_purchase": pending_purchase,
"pending_sales": pending_sales,
"low_stock_products": low_stock,
}
dashboard_service = DashboardService()