dingdanquanliucheng/backend/app/api/pricing.py
taiyi 0adccb60d2 feat: 新增 Redis 查询缓存加速
新建 cache.py 缓存工具模块,封装 Redis 读写和容错降级。
订单详情/列表、客户列表、定价规则读取走缓存,写操作后精确清除。
Redis 不可用时自动降级查 DB,不影响正常业务。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 10:37:51 +08:00

543 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
定价管理路由模块
职责:
处理定价规则、供应商成本、价格层级的增删改查以及报价计算接口URL 前缀为 /api。
包括:
- 定价规则 CRUD/pricing-rules
- 供应商成本 CRUD/supplier-costs
- 价格层级 CRUD/price-tiers
- 报价计算(/quotation/calculate
"""
import json
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from backend.app.api.deps import get_current_user, require_permissions, require_roles
from backend.app.core.cache import cache_delete_pattern, cache_get, cache_set
from backend.app.core.exceptions import AppException
from backend.app.core.error_codes import ErrorCode
from backend.app.db import get_db_session
from backend.app.models.business import (
ProductPriceTier,
ProductPricingRule,
SupplierProductCost,
)
from backend.app.schemas.common import success_payload
from backend.app.schemas.pricing import (
CreatePriceTierRequest,
CreatePricingRuleRequest,
CreateSupplierCostRequest,
QuotationCalculateRequest,
UpdatePriceTierRequest,
UpdatePricingRuleRequest,
UpdateSupplierCostRequest,
)
from backend.app.services.pricing_engine import pricing_engine
router = APIRouter(prefix="/api", tags=["pricing"])
# ======================================================================
# 定价规则 CRUD
# ======================================================================
@router.get("/pricing-rules")
def list_pricing_rules(
product_id: int | None = Query(default=None), # 产品 ID 筛选
status: int | None = Query(default=None), # 规则状态筛选(启用/停用)
page_no: int = Query(default=1), # 页码,默认第 1 页
page_size: int = Query(default=20), # 每页条数,默认 20 条
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager")), # 角色鉴权:管理员/经理
_perm: dict = Depends(require_permissions("master-data:list")), # 权限鉴权:基础数据查看
) -> dict:
"""分页查询定价规则列表
用途:获取所有产品的定价规则,支持按产品 ID 和状态筛选。
请求参数Query 参数筛选 + 分页参数。
返回值:分页定价规则列表,包含 total、page_no、page_size、list。
权限要求管理员admin或经理manager且需 master-data:list 权限。
"""
q = session.query(ProductPricingRule).filter(ProductPricingRule.deleted == 0)
if product_id is not None:
q = q.filter(ProductPricingRule.product_id == product_id)
if status is not None:
q = q.filter(ProductPricingRule.status == status)
q = q.order_by(ProductPricingRule.id.desc())
total = q.count()
items = q.offset((page_no - 1) * page_size).limit(page_size).all()
return success_payload({
"total": total,
"page_no": page_no,
"page_size": page_size,
"list": [_rule_to_dict(r) for r in items],
})
@router.get("/pricing-rules/{product_id}")
def get_pricing_rule(
product_id: int, # 产品 ID路径参数
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager")), # 角色鉴权:管理员/经理
) -> dict:
"""查询指定产品的定价规则
用途:根据产品 ID 获取该产品对应的定价规则详情。
请求参数product_id - 产品 ID路径参数
返回值:定价规则详情信息。
权限要求管理员admin或经理manager
"""
cache_key = f"pricing:rule:{product_id}"
cached = cache_get(cache_key)
if cached is not None:
return success_payload(cached)
rule = session.query(ProductPricingRule).filter(
ProductPricingRule.product_id == product_id,
ProductPricingRule.deleted == 0,
).first()
if not rule:
raise AppException(code=ErrorCode.NOT_FOUND, message="定价规则不存在", status_code=404)
rule_data = _rule_to_dict(rule)
cache_set(cache_key, rule_data, ttl=300)
return success_payload(rule_data)
@router.post("/pricing-rules")
def create_pricing_rule(
payload: CreatePricingRuleRequest, # 创建定价规则的请求体
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager")), # 角色鉴权:管理员/经理
_perm: dict = Depends(require_permissions("master-data:update")), # 权限鉴权:基础数据更新
) -> dict:
"""创建定价规则
用途:为指定产品创建一条定价规则,每个产品只能有一条生效规则。
请求参数CreatePricingRuleRequest产品 ID、定价类型、基准单价、公式等
返回值:创建成功后的定价规则信息。
权限要求管理员admin或经理manager且需 master-data:update 权限。
"""
existing = session.query(ProductPricingRule).filter(
ProductPricingRule.product_id == payload.product_id,
ProductPricingRule.deleted == 0,
).first()
if existing:
raise AppException(code=ErrorCode.PARAM_ERROR, message="该产品已存在定价规则", status_code=400)
rule = ProductPricingRule(
product_id=payload.product_id,
product_name=payload.product_name,
pricing_type=payload.pricing_type,
base_unit_price=payload.base_unit_price,
pricing_unit=payload.pricing_unit,
pricing_inputs=payload.pricing_inputs,
formula_expr=payload.formula_expr,
formula_constants=payload.formula_constants,
surcharge_json=payload.surcharge_json,
formula_note=payload.formula_note,
status=payload.status,
)
session.add(rule)
session.commit()
cache_delete_pattern("pricing:*")
session.refresh(rule)
return success_payload(_rule_to_dict(rule))
@router.put("/pricing-rules/{rule_id}")
def update_pricing_rule(
rule_id: int, # 定价规则 ID路径参数
payload: UpdatePricingRuleRequest, # 更新定价规则的请求体
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager")), # 角色鉴权:管理员/经理
_perm: dict = Depends(require_permissions("master-data:update")), # 权限鉴权:基础数据更新
) -> dict:
"""更新定价规则
用途:修改已有定价规则的公式、单价、附加费等参数。
请求参数rule_id路径参数+ UpdatePricingRuleRequest更新字段
返回值:更新后的定价规则信息。
权限要求管理员admin或经理manager且需 master-data:update 权限。
"""
rule = session.query(ProductPricingRule).filter(
ProductPricingRule.id == rule_id,
ProductPricingRule.deleted == 0,
).first()
if not rule:
raise AppException(code=ErrorCode.NOT_FOUND, message="定价规则不存在", status_code=404)
data = payload.model_dump(exclude_unset=True)
for k, v in data.items():
setattr(rule, k, v)
session.commit()
cache_delete_pattern("pricing:*")
session.refresh(rule)
return success_payload(_rule_to_dict(rule))
@router.delete("/pricing-rules/{rule_id}")
def delete_pricing_rule(
rule_id: int, # 定价规则 ID路径参数
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager")), # 角色鉴权:管理员/经理
_perm: dict = Depends(require_permissions("master-data:update")), # 权限鉴权:基础数据更新
) -> dict:
"""删除定价规则(软删除)
用途:将指定定价规则标记为已删除,不从数据库物理删除。
请求参数rule_id - 定价规则 ID路径参数
返回值:{"deleted": true}
权限要求管理员admin或经理manager且需 master-data:update 权限。
"""
rule = session.query(ProductPricingRule).filter(
ProductPricingRule.id == rule_id,
ProductPricingRule.deleted == 0,
).first()
if not rule:
raise AppException(code=ErrorCode.NOT_FOUND, message="定价规则不存在", status_code=404)
rule.deleted = 1
session.commit()
cache_delete_pattern("pricing:*")
return success_payload({"deleted": True})
# ======================================================================
# 供应商成本 CRUD
# ======================================================================
@router.get("/supplier-costs")
def list_supplier_costs(
product_id: int = Query(...), # 产品 ID必填
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager")), # 角色鉴权:管理员/经理
) -> dict:
"""查询指定产品的供应商成本列表
用途:获取某个产品在各供应商处的成本信息,按主供应商优先排序。
请求参数product_id - 产品 ID必填Query 参数)。
返回值:供应商成本列表。
权限要求管理员admin或经理manager
"""
items = session.query(SupplierProductCost).filter(
SupplierProductCost.product_id == product_id,
SupplierProductCost.deleted == 0,
).order_by(SupplierProductCost.is_primary.desc(), SupplierProductCost.id.asc()).all()
return success_payload({"list": [_cost_to_dict(c) for c in items]})
@router.post("/supplier-costs")
def create_supplier_cost(
payload: CreateSupplierCostRequest, # 创建供应商成本的请求体
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager")), # 角色鉴权:管理员/经理
) -> dict:
"""创建供应商成本记录
用途:新增某产品在某供应商处的成本价格。
请求参数CreateSupplierCostRequest产品 ID、供应商 ID、成本价、单位等
返回值:创建成功后的供应商成本信息。
权限要求管理员admin或经理manager
"""
cost = SupplierProductCost(**payload.model_dump())
session.add(cost)
session.commit()
cache_delete_pattern("pricing:*")
session.refresh(cost)
return success_payload(_cost_to_dict(cost))
@router.put("/supplier-costs/{cost_id}")
def update_supplier_cost(
cost_id: int, # 供应商成本 ID路径参数
payload: UpdateSupplierCostRequest, # 更新供应商成本的请求体
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager")), # 角色鉴权:管理员/经理
) -> dict:
"""更新供应商成本记录
用途:修改已有供应商成本的价格、备注等信息。
请求参数cost_id路径参数+ UpdateSupplierCostRequest更新字段
返回值:更新后的供应商成本信息。
权限要求管理员admin或经理manager
"""
cost = session.query(SupplierProductCost).filter(
SupplierProductCost.id == cost_id,
SupplierProductCost.deleted == 0,
).first()
if not cost:
raise AppException(code=ErrorCode.NOT_FOUND, message="供应商成本不存在", status_code=404)
data = payload.model_dump(exclude_unset=True)
for k, v in data.items():
setattr(cost, k, v)
session.commit()
cache_delete_pattern("pricing:*")
session.refresh(cost)
return success_payload(_cost_to_dict(cost))
@router.delete("/supplier-costs/{cost_id}")
def delete_supplier_cost(
cost_id: int, # 供应商成本 ID路径参数
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager")), # 角色鉴权:管理员/经理
) -> dict:
"""删除供应商成本记录(软删除)
用途:将指定供应商成本记录标记为已删除。
请求参数cost_id - 供应商成本 ID路径参数
返回值:{"deleted": true}
权限要求管理员admin或经理manager
"""
cost = session.query(SupplierProductCost).filter(
SupplierProductCost.id == cost_id,
SupplierProductCost.deleted == 0,
).first()
if not cost:
raise AppException(code=ErrorCode.NOT_FOUND, message="供应商成本不存在", status_code=404)
cost.deleted = 1
session.commit()
cache_delete_pattern("pricing:*")
return success_payload({"deleted": True})
# ======================================================================
# 价格层级 CRUD
# ======================================================================
@router.get("/price-tiers")
def list_price_tiers(
product_id: int = Query(...), # 产品 ID必填
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager", "salesman")), # 角色鉴权:所有角色可查看
) -> dict:
"""查询指定产品的价格层级列表
用途:获取某个产品的各层级售价信息(如普通价、会员价等)。
请求参数product_id - 产品 ID必填Query 参数)。
返回值:价格层级列表。
权限要求管理员admin、经理manager、业务员salesman
"""
items = session.query(ProductPriceTier).filter(
ProductPriceTier.product_id == product_id,
ProductPriceTier.deleted == 0,
).order_by(ProductPriceTier.id.asc()).all()
return success_payload({"list": [_tier_to_dict(t) for t in items]})
@router.post("/price-tiers")
def create_price_tier(
payload: CreatePriceTierRequest, # 创建价格层级的请求体
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager")), # 角色鉴权:管理员/经理
) -> dict:
"""创建价格层级
用途为指定产品新增一个价格层级如普通客户价、VIP 客户价)。
请求参数CreatePriceTierRequest产品 ID、层级编码、层级名称、价格、单位等
返回值:创建成功后的价格层级信息。
权限要求管理员admin或经理manager
"""
tier = ProductPriceTier(**payload.model_dump())
session.add(tier)
session.commit()
cache_delete_pattern("pricing:*")
session.refresh(tier)
return success_payload(_tier_to_dict(tier))
@router.put("/price-tiers/{tier_id}")
def update_price_tier(
tier_id: int, # 价格层级 ID路径参数
payload: UpdatePriceTierRequest, # 更新价格层级的请求体
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager")), # 角色鉴权:管理员/经理
) -> dict:
"""更新价格层级
用途:修改已有价格层级的名称、价格等信息。
请求参数tier_id路径参数+ UpdatePriceTierRequest更新字段
返回值:更新后的价格层级信息。
权限要求管理员admin或经理manager
"""
tier = session.query(ProductPriceTier).filter(
ProductPriceTier.id == tier_id,
ProductPriceTier.deleted == 0,
).first()
if not tier:
raise AppException(code=ErrorCode.NOT_FOUND, message="价格层级不存在", status_code=404)
data = payload.model_dump(exclude_unset=True)
for k, v in data.items():
setattr(tier, k, v)
session.commit()
cache_delete_pattern("pricing:*")
session.refresh(tier)
return success_payload(_tier_to_dict(tier))
@router.delete("/price-tiers/{tier_id}")
def delete_price_tier(
tier_id: int, # 价格层级 ID路径参数
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager")), # 角色鉴权:管理员/经理
) -> dict:
"""删除价格层级(软删除)
用途:将指定价格层级标记为已删除。
请求参数tier_id - 价格层级 ID路径参数
返回值:{"deleted": true}
权限要求管理员admin或经理manager
"""
tier = session.query(ProductPriceTier).filter(
ProductPriceTier.id == tier_id,
ProductPriceTier.deleted == 0,
).first()
if not tier:
raise AppException(code=ErrorCode.NOT_FOUND, message="价格层级不存在", status_code=404)
tier.deleted = 1
session.commit()
cache_delete_pattern("pricing:*")
return success_payload({"deleted": True})
# ======================================================================
# 报价计算
# ======================================================================
@router.post("/quotation/calculate")
def calculate_quotation(
payload: QuotationCalculateRequest, # 报价计算请求体
session: Session = Depends(get_db_session), # 注入数据库会话
_user: dict = Depends(require_roles("admin", "manager", "salesman")), # 角色鉴权:所有角色可计算
) -> dict:
"""计算产品报价
用途:根据定价规则、产品属性、客户层级和附加费选项,计算最终报价。
请求参数QuotationCalculateRequest产品 ID、客户 ID、用户输入参数、附加费选择等
返回值:报价结果,包含面积、基础成本、附加费明细、成本价、各层级售价、公式详情等。
权限要求管理员admin、经理manager、业务员salesman
"""
from backend.app.models.business import Product, Customer
# 查询产品定价规则
rule = session.query(ProductPricingRule).filter(
ProductPricingRule.product_id == payload.product_id,
ProductPricingRule.deleted == 0,
).first()
if not rule:
raise AppException(code=ErrorCode.NOT_FOUND, message="该产品未配置定价规则", status_code=404)
# 获取产品属性(厚度、克重等),用于公式计算
product = session.query(Product).filter(Product.id == payload.product_id).first()
product_attrs = {}
if product:
if product.thickness:
try:
product_attrs["thickness"] = float(product.thickness)
except (ValueError, TypeError):
pass
if product.weight_gsm:
product_attrs["weight_gsm"] = product.weight_gsm
# 合并前端传入的 surcharge 选项和自定义输入
full_inputs = {**payload.user_inputs, **payload.surcharge_selections, **payload.surcharge_inputs}
# 调用定价引擎计算
result = pricing_engine.calculate(rule, full_inputs, product_attrs)
# 查询客户所属价格层级,确定对应售价
tier_prices = {}
if payload.customer_id:
customer = session.query(Customer).filter(Customer.id == payload.customer_id).first()
tier_code = getattr(customer, "price_tier", None) or "default"
else:
tier_code = "default"
tiers = session.query(ProductPriceTier).filter(
ProductPriceTier.product_id == payload.product_id,
ProductPriceTier.deleted == 0,
).all()
for t in tiers:
tier_prices[t.tier_code] = float(t.price)
# 获取可用附加费选项列表
available_options = pricing_engine.get_available_surcharge_options(rule)
# 根据基础成本和单价反算面积
area_sqm = round(
(result.get("base_cost", 0) / float(rule.base_unit_price)) if rule.base_unit_price else 0, 4
)
return success_payload({
"product_name": rule.product_name, # 产品名称
"area_sqm": area_sqm, # 计算面积(平方米)
"base_cost": result["base_cost"], # 基础成本
"surcharge_items": result["surcharge_items"], # 附加费明细列表
"total_surcharge": result["total_surcharge"], # 附加费合计
"cost_price": result["cost_price"], # 总成本价
"sale_price_tier": tier_prices, # 各层级售价映射
"recommended_sale_price": tier_prices.get(tier_code, 0), # 推荐售价(按客户层级)
"formula_detail": result["formula_detail"], # 公式计算详情
"formula_note": rule.formula_note or "", # 公式说明
"available_surcharge_options": available_options, # 可用附加费选项
"tax_rate": result["tax_rate"], # 税率
"price_ex_tax": result["price_ex_tax"], # 不含税价
"tax_amount": result["tax_amount"], # 税额
"price_in_tax": result["price_in_tax"], # 含税价
})
# ======================================================================
# 序列化工具(将 ORM 模型转换为字典,便于返回 JSON 响应)
# ======================================================================
def _rule_to_dict(r: ProductPricingRule) -> dict:
"""将定价规则 ORM 对象序列化为字典"""
return {
"id": r.id,
"product_id": r.product_id,
"product_name": r.product_name,
"pricing_type": r.pricing_type,
"base_unit_price": float(r.base_unit_price or 0),
"pricing_unit": r.pricing_unit,
"pricing_inputs": r.pricing_inputs,
"formula_expr": r.formula_expr,
"formula_constants": r.formula_constants,
"surcharge_json": r.surcharge_json,
"formula_note": r.formula_note,
"tax_rate": float(r.tax_rate or 0),
"tax_inclusive": int(r.tax_inclusive or 0),
"status": r.status,
"created_at": r.created_at.strftime("%Y-%m-%d %H:%M:%S") if r.created_at else "",
}
def _cost_to_dict(c: SupplierProductCost) -> dict:
"""将供应商成本 ORM 对象序列化为字典"""
return {
"id": c.id,
"product_id": c.product_id,
"supplier_id": c.supplier_id,
"supplier_model": c.supplier_model,
"our_model": c.our_model,
"thickness": c.thickness,
"weight_gsm": c.weight_gsm,
"base_fabric_weight": c.base_fabric_weight,
"cost_price": float(c.cost_price or 0),
"cost_unit": c.cost_unit,
"is_primary": c.is_primary,
"remark": c.remark,
"status": c.status,
}
def _tier_to_dict(t: ProductPriceTier) -> dict:
"""将价格层级 ORM 对象序列化为字典"""
return {
"id": t.id,
"product_id": t.product_id,
"tier_code": t.tier_code,
"tier_name": t.tier_name,
"price": float(t.price or 0),
"price_unit": t.price_unit,
"remark": t.remark,
"status": t.status,
}