"""智能定价服务 实现智能定价核心业务逻辑,包含: - 综合定价计算 - AI 定价建议生成(遵循瑞小美 AI 接入规范) - 定价策略模拟 - 定价报告导出 """ import json from datetime import datetime from decimal import Decimal from typing import Optional, List, Dict, Any, AsyncIterator from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.models import ( Project, ProjectCostSummary, MarketAnalysisResult, PricingPlan, ) from app.schemas.pricing import ( StrategyType, PricingSuggestions, StrategySuggestion, MarketReference, AIAdvice, AIUsage, GeneratePricingResponse, StrategySimulationResult, SimulateStrategyResponse, ) from app.services.ai_service_wrapper import AIServiceWrapper from app.services.cost_service import CostService from app.services.market_service import MarketService # 导入提示词 import sys import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../prompts')) from prompts.pricing_advice_prompts import SYSTEM_PROMPT, USER_PROMPT, PROMPT_META class PricingService: """智能定价服务""" # 各策略的利润率范围 STRATEGY_MARGINS = { StrategyType.TRAFFIC: (0.10, 0.20), # 引流款:10%-20% StrategyType.PROFIT: (0.40, 0.60), # 利润款:40%-60% StrategyType.PREMIUM: (0.60, 0.80), # 高端款:60%-80% } STRATEGY_NAMES = { StrategyType.TRAFFIC: "引流款", StrategyType.PROFIT: "利润款", StrategyType.PREMIUM: "高端款", } def __init__(self, db: AsyncSession): self.db = db self.cost_service = CostService(db) self.market_service = MarketService(db) async def get_project_with_cost(self, project_id: int) -> tuple[Project, Optional[ProjectCostSummary]]: """获取项目及其成本汇总""" result = await self.db.execute( select(Project).options( selectinload(Project.cost_summary) ).where(Project.id == project_id) ) project = result.scalar_one_or_none() if not project: raise ValueError(f"项目不存在: {project_id}") return project, project.cost_summary async def get_market_reference(self, project_id: int) -> Optional[MarketReference]: """获取市场参考数据""" result = await self.db.execute( select(MarketAnalysisResult).where( MarketAnalysisResult.project_id == project_id ).order_by(MarketAnalysisResult.analysis_date.desc()).limit(1) ) market_result = result.scalar_one_or_none() if market_result: return MarketReference( min=float(market_result.market_min_price), max=float(market_result.market_max_price), avg=float(market_result.market_avg_price), ) return None def calculate_strategy_price( self, base_cost: float, strategy: StrategyType, target_margin: Optional[float] = None, market_ref: Optional[MarketReference] = None ) -> StrategySuggestion: """计算单个策略的定价建议 Args: base_cost: 基础成本 strategy: 策略类型 target_margin: 自定义目标毛利率(可选) market_ref: 市场参考(可选) Returns: 策略定价建议 """ min_margin, max_margin = self.STRATEGY_MARGINS[strategy] # 使用策略默认的中间利润率,或自定义目标 if target_margin is not None: margin = target_margin / 100 else: margin = (min_margin + max_margin) / 2 # 成本加成定价法:价格 = 成本 / (1 - 毛利率) suggested_price = base_cost / (1 - margin) # 如果有市场参考,调整价格 if market_ref: if strategy == StrategyType.TRAFFIC: # 引流款:取市场最低价和成本定价的较低者 market_low = market_ref.min * 0.9 suggested_price = min(suggested_price, market_low) elif strategy == StrategyType.PREMIUM: # 高端款:取市场高位 market_high = market_ref.max * 1.1 suggested_price = max(suggested_price, market_high * 0.9) # 确保价格不低于成本(边界处理:零成本时设置最低价格) if base_cost == 0: # 零成本时,使用市场参考或设置一个最低保护价格 if market_ref: suggested_price = market_ref.avg * 0.8 else: suggested_price = 1.0 # 最低保护价格 else: suggested_price = max(suggested_price, base_cost * 1.05) # 计算实际毛利率(防止除零) if suggested_price > 0: actual_margin = (suggested_price - base_cost) / suggested_price * 100 else: actual_margin = 0 descriptions = { StrategyType.TRAFFIC: "低于市场均价,适合引流获客、新店开业、淡季促销", StrategyType.PROFIT: "接近市场均价,平衡利润与竞争力,适合日常经营", StrategyType.PREMIUM: "定位高端,高利润空间,需配套优质服务和品牌溢价", } return StrategySuggestion( strategy=self.STRATEGY_NAMES[strategy], suggested_price=round(suggested_price, 2), margin=round(actual_margin, 1), description=descriptions[strategy], ) def calculate_all_strategies( self, base_cost: float, target_margin: float, market_ref: Optional[MarketReference] = None, strategies: Optional[List[StrategyType]] = None ) -> PricingSuggestions: """计算所有策略的定价建议""" if strategies is None: strategies = list(StrategyType) suggestions = PricingSuggestions() for strategy in strategies: suggestion = self.calculate_strategy_price( base_cost=base_cost, strategy=strategy, target_margin=target_margin if strategy == StrategyType.PROFIT else None, market_ref=market_ref, ) if strategy == StrategyType.TRAFFIC: suggestions.traffic = suggestion elif strategy == StrategyType.PROFIT: suggestions.profit = suggestion elif strategy == StrategyType.PREMIUM: suggestions.premium = suggestion return suggestions async def generate_pricing_advice( self, project_id: int, target_margin: float = 50, strategies: Optional[List[StrategyType]] = None, stream: bool = False, ) -> GeneratePricingResponse: """生成智能定价建议 遵循瑞小美 AI 接入规范: - 通过 AIServiceWrapper 调用 - 必须传入 prompt_name(用于统计) Args: project_id: 项目ID target_margin: 目标毛利率 strategies: 要计算的策略列表 stream: 是否流式输出(此方法返回完整结果,流式由路由处理) Returns: 定价建议响应 """ # 获取项目和成本数据 project, cost_summary = await self.get_project_with_cost(project_id) if not cost_summary: # 如果没有成本汇总,先计算 cost_result = await self.cost_service.calculate_project_cost(project_id) base_cost = cost_result.total_cost else: base_cost = float(cost_summary.total_cost) # 获取市场参考 market_ref = await self.get_market_reference(project_id) # 计算各策略价格 pricing_suggestions = self.calculate_all_strategies( base_cost=base_cost, target_margin=target_margin, market_ref=market_ref, strategies=strategies, ) # 构建 AI 输入数据 cost_data = self._format_cost_data(cost_summary) market_data = self._format_market_data(market_ref) # 调用 AI 生成建议(遵循规范) ai_service = AIServiceWrapper(db_session=self.db) messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": USER_PROMPT.format( project_name=project.project_name, cost_data=cost_data, market_data=market_data, target_margin=target_margin, )}, ] ai_advice = None ai_usage = None try: response = await ai_service.chat( messages=messages, prompt_name=PROMPT_META["name"], # 必填!用于调用统计 ) ai_advice = AIAdvice( summary=self._extract_section(response.content, "推荐方案") or response.content[:200], cost_analysis=self._extract_section(response.content, "成本") or "", market_analysis=self._extract_section(response.content, "市场") or "", risk_notes=self._extract_section(response.content, "风险") or "", recommendations=self._extract_recommendations(response.content), ) ai_usage = AIUsage( provider=response.provider, model=response.model, tokens=response.total_tokens, latency_ms=response.latency_ms, ) except Exception as e: # AI 调用失败不影响基本定价计算 print(f"AI 调用失败: {e}") return GeneratePricingResponse( project_id=project_id, project_name=project.project_name, cost_base=base_cost, market_reference=market_ref, pricing_suggestions=pricing_suggestions, ai_advice=ai_advice, ai_usage=ai_usage, ) async def generate_pricing_advice_stream( self, project_id: int, target_margin: float = 50, ) -> AsyncIterator[str]: """流式生成定价建议 Args: project_id: 项目ID target_margin: 目标毛利率 Yields: SSE 格式的响应片段 """ # 获取基础数据 project, cost_summary = await self.get_project_with_cost(project_id) if not cost_summary: cost_result = await self.cost_service.calculate_project_cost(project_id) base_cost = cost_result.total_cost else: base_cost = float(cost_summary.total_cost) market_ref = await self.get_market_reference(project_id) # 先返回基础定价计算结果 pricing_suggestions = self.calculate_all_strategies( base_cost=base_cost, target_margin=target_margin, market_ref=market_ref, ) # 发送初始数据 initial_data = { "type": "init", "project_name": project.project_name, "cost_base": base_cost, "market_reference": market_ref.model_dump() if market_ref else None, "pricing_suggestions": pricing_suggestions.model_dump(), } yield f"data: {json.dumps(initial_data, ensure_ascii=False)}\n\n" # 构建 AI 输入 cost_data = self._format_cost_data(cost_summary) market_data = self._format_market_data(market_ref) ai_service = AIServiceWrapper(db_session=self.db) messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": USER_PROMPT.format( project_name=project.project_name, cost_data=cost_data, market_data=market_data, target_margin=target_margin, )}, ] # 流式返回 AI 建议 try: async for chunk in ai_service.chat_stream( messages=messages, prompt_name=PROMPT_META["name"], ): yield f"data: {json.dumps({'type': 'chunk', 'content': chunk}, ensure_ascii=False)}\n\n" except Exception as e: yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n" # 发送完成信号 yield f"data: {json.dumps({'type': 'done'}, ensure_ascii=False)}\n\n" async def simulate_strategies( self, project_id: int, strategies: List[StrategyType], target_margin: float = 50, ) -> SimulateStrategyResponse: """模拟定价策略 Args: project_id: 项目ID strategies: 要模拟的策略列表 target_margin: 目标毛利率 Returns: 策略模拟结果 """ project, cost_summary = await self.get_project_with_cost(project_id) if not cost_summary: cost_result = await self.cost_service.calculate_project_cost(project_id) base_cost = cost_result.total_cost else: base_cost = float(cost_summary.total_cost) market_ref = await self.get_market_reference(project_id) results = [] for strategy in strategies: suggestion = self.calculate_strategy_price( base_cost=base_cost, strategy=strategy, target_margin=target_margin if strategy == StrategyType.PROFIT else None, market_ref=market_ref, ) # 确定市场位置 market_position = "中等" if market_ref: if suggestion.suggested_price < market_ref.avg * 0.8: market_position = "低于市场均价" elif suggestion.suggested_price > market_ref.avg * 1.2: market_position = "高于市场均价" else: market_position = "接近市场均价" results.append(StrategySimulationResult( strategy_type=strategy.value, strategy_name=self.STRATEGY_NAMES[strategy], suggested_price=suggestion.suggested_price, margin=suggestion.margin, profit_per_unit=round(suggestion.suggested_price - base_cost, 2), market_position=market_position, )) return SimulateStrategyResponse( project_id=project_id, project_name=project.project_name, base_cost=base_cost, results=results, ) async def create_pricing_plan( self, project_id: int, plan_name: str, strategy_type: StrategyType, target_margin: float, created_by: Optional[int] = None, ) -> PricingPlan: """创建定价方案 Args: project_id: 项目ID plan_name: 方案名称 strategy_type: 策略类型 target_margin: 目标毛利率 created_by: 创建人ID Returns: 创建的定价方案 """ # 获取成本数据 project, cost_summary = await self.get_project_with_cost(project_id) if not cost_summary: cost_result = await self.cost_service.calculate_project_cost(project_id) base_cost = Decimal(str(cost_result.total_cost)) else: base_cost = cost_summary.total_cost # 获取市场参考 market_ref = await self.get_market_reference(project_id) # 计算建议价格 suggestion = self.calculate_strategy_price( base_cost=float(base_cost), strategy=strategy_type, target_margin=target_margin if strategy_type == StrategyType.PROFIT else None, market_ref=market_ref, ) # 创建定价方案 pricing_plan = PricingPlan( project_id=project_id, plan_name=plan_name, strategy_type=strategy_type.value, base_cost=base_cost, target_margin=Decimal(str(target_margin)), suggested_price=Decimal(str(suggestion.suggested_price)), is_active=True, created_by=created_by, ) self.db.add(pricing_plan) await self.db.flush() await self.db.refresh(pricing_plan) return pricing_plan async def update_pricing_plan( self, plan_id: int, **kwargs ) -> PricingPlan: """更新定价方案""" result = await self.db.execute( select(PricingPlan).where(PricingPlan.id == plan_id) ) plan = result.scalar_one_or_none() if not plan: raise ValueError(f"定价方案不存在: {plan_id}") for key, value in kwargs.items(): if value is not None and hasattr(plan, key): if key in ['target_margin', 'final_price', 'base_cost', 'suggested_price']: setattr(plan, key, Decimal(str(value))) else: setattr(plan, key, value) await self.db.flush() await self.db.refresh(plan) return plan async def save_ai_advice(self, plan_id: int, advice: str) -> None: """保存 AI 建议到定价方案""" result = await self.db.execute( select(PricingPlan).where(PricingPlan.id == plan_id) ) plan = result.scalar_one_or_none() if plan: plan.ai_advice = advice await self.db.flush() def _format_cost_data(self, cost_summary: Optional[ProjectCostSummary]) -> str: """格式化成本数据用于 AI 输入""" if not cost_summary: return "暂无成本数据" return f"""- 耗材成本:{float(cost_summary.material_cost):.2f} 元 - 设备折旧:{float(cost_summary.equipment_cost):.2f} 元 - 人工成本:{float(cost_summary.labor_cost):.2f} 元 - 固定成本分摊:{float(cost_summary.fixed_cost_allocation):.2f} 元 - **总成本(最低成本线):{float(cost_summary.total_cost):.2f} 元**""" def _format_market_data(self, market_ref: Optional[MarketReference]) -> str: """格式化市场数据用于 AI 输入""" if not market_ref: return "暂无市场行情数据" return f"""- 市场最低价:{market_ref.min:.2f} 元 - 市场最高价:{market_ref.max:.2f} 元 - 市场均价:{market_ref.avg:.2f} 元""" def _extract_section(self, content: str, keyword: str) -> Optional[str]: """从 AI 响应中提取特定部分""" lines = content.split('\n') in_section = False section_lines = [] for line in lines: if keyword in line and ('#' in line or '**' in line): in_section = True continue elif in_section: if line.startswith('#') or (line.startswith('**') and line.endswith('**')): break section_lines.append(line) return '\n'.join(section_lines).strip() if section_lines else None def _extract_recommendations(self, content: str) -> List[str]: """从 AI 响应中提取建议列表""" recommendations = [] lines = content.split('\n') for line in lines: line = line.strip() if line.startswith('- ') or line.startswith('* ') or line.startswith('• '): recommendations.append(line[2:].strip()) elif line and line[0].isdigit() and '.' in line: # 处理 "1. xxx" 格式 parts = line.split('.', 1) if len(parts) > 1: recommendations.append(parts[1].strip()) return recommendations[:5] # 最多返回5条