import json from typing import Any, Dict, List, Optional from tortoise.expressions import Q from tortoise.queryset import QuerySet from tortoise.functions import Count from app.models.valuation import ValuationAssessment, ValuationCalculationStep from app.schemas.valuation import ( ValuationAssessmentCreate, ValuationAssessmentUpdate, ValuationAssessmentQuery, ValuationAssessmentOut, ValuationAssessmentList, ValuationCalculationStepCreate, ValuationCalculationStepOut ) from app.models.user import AppUser from app.utils.calculation_engine.formula_registry import get_formula_meta class ValuationController: """估值评估控制器""" model = ValuationAssessment step_model = ValuationCalculationStep # 参数说明映射表:将参数名(英文)映射到中文说明 PARAM_DESCRIPTIONS = { # 财务价值相关 "three_year_income": "近三年收益(万元)", "annual_revenue_3_years": "近三年收益(万元)", "financial_value_f": "财务价值F", # 法律强度相关 "patent_score": "专利分", "popularity_score": "普及分", "infringement_score": "侵权分", "legal_strength_l": "法律强度L", # 发展潜力相关 "patent_count": "专利数量", "esg_score": "ESG分", "innovation_ratio": "创新投入比", "development_potential_d": "发展潜力D", # 行业系数 "industry_coefficient": "行业系数I", "target_industry_roe": "目标行业ROE", "benchmark_industry_roe": "基准行业ROE", # 流量因子相关 "search_index_s1": "搜索指数S1", "industry_average_s2": "行业均值S2", "social_media_spread_s3": "社交媒体传播度S3", "likes": "点赞数", "comments": "评论数", "shares": "转发数", "sales_volume": "销售量", "link_views": "链接浏览量", # 政策乘数相关 "implementation_stage": "实施阶段评分", "funding_support": "资金支持度", "policy_match_score": "政策匹配度", # 文化价值相关 "inheritor_level_coefficient": "传承人等级系数", "offline_sessions": "线下传习次数", "douyin_views": "抖音浏览量", "bilibili_views": "B站浏览量", "kuaishou_views": "快手浏览量", "cross_border_depth": "跨界合作深度", "historical_inheritance": "历史传承度HI", "structure_complexity": "结构复杂度SC", "normalized_entropy": "归一化信息熵H", # 风险调整相关 "highest_price": "最高价格", "lowest_price": "最低价格", "inheritor_ages": "传承人年龄列表", "lawsuit_status": "诉讼状态", # 市场估值相关 "manual_bids": "手动竞价列表", "expert_valuations": "专家估值列表", "weighted_average_price": "加权平均价格", "daily_browse_volume": "日均浏览量", "collection_count": "收藏数", "issuance_level": "发行量", "recent_market_activity": "最近市场活动时间", # 动态质押率相关 "monthly_transaction_amount": "月交易额(万元)", "monthly_amount": "月交易额(万元)", "heritage_asset_level": "非遗等级", "dynamic_pledge_rate": "动态质押率", "base_pledge_rate": "基础质押率", "flow_correction": "流量修正系数", } async def create_calculation_step(self, data: ValuationCalculationStepCreate) -> ValuationCalculationStepOut: """ 创建估值计算步骤 Args: data (ValuationCalculationStepCreate): 估值计算步骤数据 Returns: ValuationCalculationStepOut: 创建的估值计算步骤 """ step = await self.step_model.create(**data.model_dump()) logger.info( "calcstep.create valuation_id={} order={} name={}", data.valuation_id, data.step_order, data.step_name, ) return ValuationCalculationStepOut.model_validate(step) async def log_formula_step( self, valuation_id: int, formula_code: str, *, status: str = "processing", input_params: Optional[Dict[str, Any]] = None, output_result: Optional[Dict[str, Any]] = None, error_message: Optional[str] = None, step_description: Optional[str] = None, duration_ms: Optional[int] = None, ) -> ValuationCalculationStepOut: """ 幂等记录(或更新)某个公式节点的计算过程。 """ meta = get_formula_meta(formula_code) description = step_description or meta.formula create_payload: Dict[str, Any] = { "valuation_id": valuation_id, "formula_code": meta.code, "formula_name": meta.name, "formula_text": meta.formula, "parent_formula_code": meta.parent_code, "group_code": meta.group_code, "step_order": meta.order, "step_name": meta.name, "step_description": description, "status": status, } if input_params is not None: create_payload["input_params"] = input_params if output_result is not None: create_payload["output_result"] = output_result if error_message is not None: create_payload["error_message"] = error_message # 准备更新字段 update_fields: Dict[str, Any] = { "status": status, "step_description": description, "formula_name": meta.name, "formula_text": meta.formula, "parent_formula_code": meta.parent_code, "group_code": meta.group_code, "step_order": meta.order, "step_name": meta.name, } if input_params is not None: update_fields["input_params"] = input_params if output_result is not None: update_fields["output_result"] = output_result if error_message is not None: update_fields["error_message"] = error_message if duration_ms is not None: result = update_fields.get("output_result") or {} if not isinstance(result, dict): result = {} result["duration_ms"] = duration_ms update_fields["output_result"] = result # 先尝试查询是否存在(明确排除 formula_code 为 NULL 的情况) step = await self.step_model.filter( valuation_id=valuation_id, formula_code=meta.code ).first() # 如果没找到,再检查是否有 formula_code 为 NULL 的旧记录(不应该有,但为了安全) if not step and meta.code: # 检查是否有重复的旧记录(formula_code 为 NULL) old_steps = await self.step_model.filter( valuation_id=valuation_id, formula_code__isnull=True ).all() if old_steps: logger.warning( "calcstep.log_formula found old records with NULL formula_code: valuation_id={} count={}", valuation_id, len(old_steps), ) logger.info( "calcstep.log_formula query: valuation_id={} formula_code={} found={}", valuation_id, meta.code, step is not None, ) if step: # 更新现有记录 await step.update_from_dict(update_fields).save() logger.info( "calcstep.log_formula updated valuation_id={} formula_code={}", valuation_id, meta.code, ) else: # 尝试创建新记录 if duration_ms is not None: result = create_payload.setdefault("output_result", {}) or {} if not isinstance(result, dict): result = {} result["duration_ms"] = duration_ms create_payload["output_result"] = result try: step = await self.step_model.create(**create_payload) logger.info( "calcstep.log_formula created valuation_id={} formula_code={}", valuation_id, meta.code, ) except Exception as e: # 如果因为唯一约束冲突而失败(可能是并发插入),重新查询并更新 error_str = str(e).lower() if "duplicate" in error_str or "unique" in error_str or "1062" in error_str: logger.warning( "calcstep.log_formula duplicate key detected, retrying query: {}", str(e), ) # 重新查询(可能已被其他请求插入) step = await self.step_model.filter( valuation_id=valuation_id, formula_code=meta.code ).first() if step: # 更新刚插入的记录 await step.update_from_dict(update_fields).save() logger.info( "calcstep.log_formula updated after duplicate key: valuation_id={} formula_code={}", valuation_id, meta.code, ) else: # 如果还是找不到,记录错误但继续 logger.error( "calcstep.log_formula failed to find record after duplicate key error: valuation_id={} formula_code={}", valuation_id, meta.code, ) raise else: # 其他错误直接抛出 raise return ValuationCalculationStepOut.model_validate(step) async def update_calculation_step(self, step_id: int, update: dict) -> ValuationCalculationStepOut: step = await self.step_model.filter(id=step_id).first() if not step: raise ValueError(f"calculation_step not found: {step_id}") await step.update_from_dict(update).save() logger.info( "calcstep.update id={} fields={}", step_id, list(update.keys()), ) return ValuationCalculationStepOut.model_validate(step) async def get_calculation_steps(self, valuation_id: int) -> List[ValuationCalculationStepOut]: """ 根据估值ID获取所有相关的计算步骤。 此方法从数据库中检索与特定估值ID关联的所有计算步骤记录, 并按创建时间升序排序,确保步骤的顺序正确。 Args: valuation_id (int): 估值的唯一标识符。 Returns: List[ValuationCalculationStepOut]: 一个包含所有相关计算步骤的列表, 如果找不到任何步骤,则返回空列表。 """ steps = await self.step_model.filter(valuation_id=valuation_id).order_by('created_at') logger.info("calcstep.list valuation_id={} count={}", valuation_id, len(steps)) return [ValuationCalculationStepOut.model_validate(step) for step in steps] async def get_calculation_report_markdown(self, valuation_id: int) -> str: """ 根据估值ID生成计算过程的 Markdown 报告。 此方法会查询所有相关的计算步骤,按照公式顺序组织, 并生成格式化的 Markdown 文档,包含: - 公式名称 - 输入参数 - 公式文本 - 输出结果 Args: valuation_id (int): 估值的唯一标识符。 Returns: str: Markdown 格式的计算报告。 Raises: ValueError: 如果找不到对应的估值记录。 """ # 验证估值记录是否存在 valuation = await self.model.filter(id=valuation_id, is_active=True).first() if not valuation: raise ValueError(f"估值记录不存在: {valuation_id}") # 获取所有计算步骤,按 step_order 排序 steps = await self.step_model.filter(valuation_id=valuation_id).order_by('step_order') if not steps: return f"# 计算摘要\n\n**估值ID**: {valuation_id}\n\n**资产名称**: {valuation.asset_name}\n\n> 暂无计算步骤记录。\n" # 转换为字典列表,便于处理 steps_data = [] for step in steps: step_dict = ValuationCalculationStepOut.model_validate(step).model_dump() steps_data.append(step_dict) # 生成 Markdown markdown = self._generate_markdown(valuation, steps_data) logger.info("calcstep.report_markdown generated valuation_id={} steps_count={}", valuation_id, len(steps_data)) return markdown def _build_formula_tree(self, steps: List[Dict]) -> Dict: """ 构建公式的树形结构。 Args: steps: 计算步骤列表。 Returns: Dict: 树形结构的字典,key 为 formula_code,value 为步骤数据和子节点。 """ # 按 formula_code 索引 step_map = {} for step in steps: code = step.get('formula_code') if code: step_map[code] = step # 构建树形结构 tree = {} processed = set() # 第一遍:创建所有节点 for step in steps: code = step.get('formula_code') if not code or code in processed: continue node = { 'step': step, 'children': [] } tree[code] = node processed.add(code) # 第二遍:建立父子关系 root_nodes = [] for step in steps: code = step.get('formula_code') if not code: continue parent_code = step.get('parent_formula_code') node = tree[code] if parent_code and parent_code in tree: # 有父节点,添加到父节点的 children tree[parent_code]['children'].append(node) else: # 根节点 root_nodes.append(node) # 按 step_order 排序 def sort_nodes(nodes): nodes.sort(key=lambda n: float(n['step'].get('step_order', 0))) for node in nodes: if node['children']: sort_nodes(node['children']) sort_nodes(root_nodes) return {'roots': root_nodes, 'all': tree} def _generate_markdown(self, valuation, steps_data: List[Dict]) -> str: """ 生成 Markdown 格式的报告。 Args: valuation: 估值评估对象。 steps_data: 计算步骤列表(已按 step_order 排序)。 Returns: str: Markdown 格式的字符串。 """ lines = [] # 标题 lines.append("# 计算摘要") lines.append("") lines.append("") # 遍历所有步骤,按顺序生成 for step in steps_data: name = step.get('formula_name', step.get('step_name', '未知')) formula_text = step.get('formula_text', step.get('step_description', '')) input_params = step.get('input_params') output_result = step.get('output_result') # 公式标题(二级标题) lines.append(f"## {name}") lines.append("") # 参数部分 if input_params: lines.append("**参数:**") lines.append("") # 格式化参数显示 param_lines = self._format_params(input_params) lines.extend(param_lines) lines.append("") # 公式部分 if formula_text: lines.append("**公式:**") lines.append("") lines.append("```") lines.append(formula_text) lines.append("```") lines.append("") # 计算过程部分(显示详细的计算步骤) if output_result and isinstance(output_result, dict): # 首先检查 calculation_detail 字段 calculation_detail = output_result.get('calculation_detail') if calculation_detail and isinstance(calculation_detail, dict): lines.append("**计算过程:**") lines.append("") # 按步骤顺序显示 steps = [] for key in sorted(calculation_detail.keys()): if key.startswith('step'): steps.append(f"> {calculation_detail[key]}") if steps: lines.extend(steps) lines.append("") # 然后检查旧的 calculation 字段 calculation = output_result.get('calculation') if calculation and not calculation_detail: lines.append("**计算过程:**") lines.append("") lines.append(f"> {calculation}") lines.append("") # 结果部分 if output_result: # 提取主要结果值 result_value = self._extract_main_result(output_result, name) if result_value is not None: lines.append("**结果:**") lines.append("") lines.append(f"`{result_value}`") lines.append("") lines.append("") return "\n".join(lines) def _format_params(self, params: Dict[str, Any]) -> List[str]: """ 格式化参数显示,优先使用列表格式(如果是数组),否则显示为列表项。 参数名会附带中文说明(如果存在)。 Args: params: 参数字典 Returns: List[str]: 格式化后的参数行列表 """ lines = [] def _get_param_label(key: str) -> str: """获取参数标签,包含中文说明""" description = self.PARAM_DESCRIPTIONS.get(key) if description: return f"{key}({description})" return key # 如果参数只有一个键,且值是数组,直接显示数组(不带参数名,符合示例格式) if len(params) == 1: key, value = next(iter(params.items())) if isinstance(value, (list, tuple)): # 格式化为列表:- [12.2, 13.2, 14.2] value_str = json.dumps(list(value), ensure_ascii=False) lines.append(f"- {value_str}") return lines # 多个参数或非数组,显示为列表项(带说明) for key, value in params.items(): param_label = _get_param_label(key) if isinstance(value, (list, tuple)): value_str = json.dumps(list(value), ensure_ascii=False) lines.append(f"- **{param_label}**: {value_str}") elif isinstance(value, dict): value_str = json.dumps(value, ensure_ascii=False) lines.append(f"- **{param_label}**: {value_str}") else: lines.append(f"- **{param_label}**: {value}") return lines def _extract_main_result(self, output_result: Dict[str, Any], formula_name: str) -> Optional[str]: """ 从输出结果中提取主要结果值。 优先顺序: 1. 如果结果中只有一个数值类型的值,返回该值 2. 如果结果中包含与公式名称相关的字段(如 "财务价值 F" -> "financial_value_f"),返回该值 3. 如果结果中包含常见的计算结果字段(如 "result", "value", "output"),返回该值 4. 返回第一个数值类型的值 Args: output_result: 输出结果字典 formula_name: 公式名称 Returns: Optional[str]: 主要结果值的字符串表示,如果找不到则返回 None """ if not output_result or not isinstance(output_result, dict): return None # 调试:打印B3的output_result if 'risk_value_b3' in str(output_result) or 'legal_risk' in str(output_result): print(f"=== _extract_main_result 调试 ===") print(f"formula_name: {formula_name}") print(f"output_result keys: {list(output_result.keys())}") print(f"output_result: {output_result}") print(f"================================") # 移除 duration_ms 等元数据字段 filtered_result = {k: v for k, v in output_result.items() if k not in ['duration_ms', 'duration', 'timestamp', 'status']} if not filtered_result: return None # 如果只有一个值,直接返回 if len(filtered_result) == 1: value = next(iter(filtered_result.values())) if isinstance(value, (int, float)): return str(value) elif isinstance(value, (list, tuple)) and len(value) == 1: return str(value[0]) else: return json.dumps(value, ensure_ascii=False) # 优先查找常见的结果字段(优先级从高到低) # 这个列表的顺序很重要,确保正确的结果字段优先被选中 common_result_keys = [ # 计算引擎实际使用的结果字段名 'risk_value_b3', # 风险调整系数B3 'risk_adjustment_b3', # 风险调整系数B3(备选) 'economic_value_b1', # 经济价值B1 'cultural_value_b2', # 文化价值B2 'model_value_b', # 模型估值B 'market_value_c', # 市场估值C 'final_value_a', # 最终估值A 'final_value_ab', # 最终估值AB 'basic_value_b11', # 基础价值B11 'traffic_factor_b12', # 流量因子B12 'policy_multiplier_b13', # 政策乘数B13 'living_heritage_b21', # 活态传承系数B21 'pattern_gene_b22', # 纹样基因值B22 # 通用结果字段 'result', 'value', 'output', 'final_value', 'calculated_value', # 子计算结果字段 'financial_value_f', 'legal_strength_l', 'development_potential_d', 'social_media_spread_s3', 'interaction_index', 'coverage_index', 'conversion_efficiency', 'market_bid_c1', 'heat_coefficient_c2', 'scarcity_multiplier_c3', 'timeliness_decay_c4', 'teaching_frequency', 'inheritor_level_coefficient', 'risk_score_sum', # 风险评分总和R 'dynamic_pledge_rate', # 动态质押率 ] # 首先检查常见结果字段(这个优先级最高,避免错误匹配子风险值) for key in common_result_keys: if key in filtered_result: value = filtered_result[key] if isinstance(value, (int, float)): if 'risk' in formula_name.lower() or 'b3' in formula_name.lower(): print(f"=== 返回值调试 (common_keys) ===") print(f"formula_name: {formula_name}") print(f"matched key: {key}") print(f"返回值: {value}") print(f"================================") return str(value) # 尝试根据公式名称匹配字段 # 例如:"财务价值 F" -> 查找 "financial_value_f", "财务价值F" 等 # 提取公式名称中的关键部分(通常是最后一个字母或单词) name_parts = formula_name.split() if name_parts: # 获取最后一个部分(通常是字母,如 "F", "L", "D") last_part = name_parts[-1].lower() # 构建可能的字段名:如 "financial_value_f", "legal_strength_l" 等 # 将中文名称转换为可能的英文字段名模式 possible_keys = [] # 1. 直接匹配包含最后部分的字段(如包含 "f", "l", "d") for key in filtered_result.keys(): if last_part in key.lower() or key.lower().endswith(f"_{last_part}"): possible_keys.append(key) # 2. 尝试匹配常见的命名模式 # 例如:"财务价值 F" -> "financial_value_f" # 这里我们尝试匹配以最后部分结尾的字段 suffix_patterns = [ f"_{last_part}", f"_{last_part}_", last_part, ] for key in filtered_result.keys(): key_lower = key.lower() for pattern in suffix_patterns: if key_lower.endswith(pattern) or pattern in key_lower: if key not in possible_keys: possible_keys.append(key) # 按优先级匹配 for key in possible_keys: if key in filtered_result: value = filtered_result[key] if isinstance(value, (int, float)): return str(value) # 返回第一个数值类型的值 for key, value in filtered_result.items(): if isinstance(value, (int, float)): return str(value) # 如果都没有,返回整个结果的 JSON(但简化显示) return json.dumps(filtered_result, ensure_ascii=False) async def create(self, data: ValuationAssessmentCreate, user_id: int) -> ValuationAssessmentOut: """创建估值评估""" # 将用户ID添加到数据中 create_data = data.model_dump() create_data['user_id'] = user_id valuation = await self.model.create(**create_data) out = ValuationAssessmentOut.model_validate(valuation) return await self._attach_user_phone(out) async def get_by_id(self, valuation_id: int) -> Optional[ValuationAssessmentOut]: """根据ID获取估值评估""" valuation = await self.model.filter(id=valuation_id, is_active=True).first() if valuation: out = ValuationAssessmentOut.model_validate(valuation) return await self._attach_user_phone(out) return None async def update(self, valuation_id: int, data: ValuationAssessmentUpdate) -> Optional[ValuationAssessmentOut]: """更新估值评估""" valuation = await self.model.filter(id=valuation_id, is_active=True).first() if not valuation: return None update_data = data.model_dump(exclude_unset=True) if update_data: if 'certificate_url' in update_data and update_data.get('certificate_url'): from datetime import datetime update_data['audited_at'] = datetime.now() update_data['updated_at'] = datetime.now() else: from datetime import datetime update_data['updated_at'] = datetime.now() await valuation.update_from_dict(update_data) await valuation.save() from datetime import datetime valuation.status ="pending" if not getattr(valuation, "audited_at", None): valuation.audited_at = datetime.now() valuation.updated_at = datetime.now() await valuation.save() out = ValuationAssessmentOut.model_validate(valuation) return await self._attach_user_phone(out) async def update1(self, valuation_id: int, data: ValuationAssessmentUpdate) -> Optional[ValuationAssessmentOut]: """更新估值评估""" valuation = await self.model.filter(id=valuation_id, is_active=True).first() if not valuation: return None update_data = data.model_dump(exclude_unset=True) if update_data: if 'certificate_url' in update_data and update_data.get('certificate_url'): from datetime import datetime update_data['audited_at'] = datetime.now() update_data['updated_at'] = datetime.now() else: from datetime import datetime update_data['updated_at'] = datetime.now() await valuation.update_from_dict(update_data) await valuation.save() from datetime import datetime valuation.status ="success" if not getattr(valuation, "audited_at", None): valuation.audited_at = datetime.now() valuation.updated_at = datetime.now() await valuation.save() out = ValuationAssessmentOut.model_validate(valuation) return await self._attach_user_phone(out) async def delete(self, valuation_id: int) -> bool: """软删除估值评估""" valuation = await self.model.filter(id=valuation_id, is_active=True).first() if not valuation: return False valuation.is_active = False await valuation.save() return True async def get_list(self, query: ValuationAssessmentQuery) -> ValuationAssessmentList: """获取估值评估列表""" queryset = self._build_query(query) # 计算总数 total = await queryset.count() # 分页查询 offset = (query.page - 1) * query.size valuations = await queryset.offset(offset).limit(query.size).order_by('-created_at') # 转换为输出模型 items = [ValuationAssessmentOut.model_validate(v) for v in valuations] items = await self._attach_user_phone_bulk(items) # 计算总页数 pages = (total + query.size - 1) // query.size return ValuationAssessmentList( items=items, total=total, page=query.page, size=query.size, pages=pages ) def _build_query(self, query: ValuationAssessmentQuery) -> QuerySet: """构建查询条件""" queryset = self.model.filter(is_active=True) if query.asset_name: queryset = queryset.filter(asset_name__icontains=query.asset_name) if query.institution: queryset = queryset.filter(institution__icontains=query.institution) if query.industry: queryset = queryset.filter(industry__icontains=query.industry) if query.heritage_level: queryset = queryset.filter(heritage_level__icontains=query.heritage_level) if query.is_active is not None: queryset = queryset.filter(is_active=query.is_active) # 添加状态筛选 if hasattr(query, 'status') and query.status: queryset = queryset.filter(status=query.status) if getattr(query, 'phone', None): queryset = queryset.filter(user__phone__icontains=query.phone) def _parse_time(v: Optional[str]): if not v: return None try: iv = int(v) from datetime import datetime return datetime.fromtimestamp(iv / 1000) except Exception: try: from datetime import datetime return datetime.fromisoformat(v) except Exception: return None s_dt = _parse_time(getattr(query, 'submitted_start', None)) e_dt = _parse_time(getattr(query, 'submitted_end', None)) if s_dt: queryset = queryset.filter(created_at__gte=s_dt) if e_dt: queryset = queryset.filter(created_at__lte=e_dt) a_s_dt = _parse_time(getattr(query, 'audited_start', None)) a_e_dt = _parse_time(getattr(query, 'audited_end', None)) if a_s_dt: queryset = queryset.filter(updated_at__gte=a_s_dt) if a_e_dt: queryset = queryset.filter(updated_at__lte=a_e_dt) return queryset async def get_statistics(self) -> dict: """获取统计信息""" total_count = await self.model.filter(is_active=True).count() # 按行业统计 industry_stats = await self.model.filter(is_active=True).group_by('industry').annotate(count=Count('id')).values('industry', 'count') # 按非遗等级统计 heritage_level_stats = await self.model.filter( is_active=True, heritage_level__isnull=False ).group_by('heritage_level').annotate(count=Count('id')).values('heritage_level', 'count') return { 'total_count': total_count, 'industry_distribution': industry_stats, 'heritage_level_distribution': heritage_level_stats } async def search(self, keyword: str, page: int = 1, size: int = 10) -> ValuationAssessmentList: """全文搜索""" queryset = self.model.filter( Q(asset_name__icontains=keyword) | Q(institution__icontains=keyword) | Q(industry__icontains=keyword) | Q(heritage_level__icontains=keyword), is_active=True ) # 计算总数 total = await queryset.count() # 分页查询 offset = (page - 1) * size valuations = await queryset.offset(offset).limit(size).order_by('-created_at') # 转换为输出模型 items = [ValuationAssessmentOut.model_validate(v) for v in valuations] items = await self._attach_user_phone_bulk(items) # 计算总页数 pages = (total + size - 1) // size return ValuationAssessmentList( items=items, total=total, page=page, size=size, pages=pages ) async def approve_valuation(self, valuation_id: int, admin_notes: Optional[str] = None) -> Optional[ValuationAssessmentOut]: """审核通过估值评估""" valuation = await self.model.filter(id=valuation_id, is_active=True).first() if not valuation: return None from datetime import datetime update_data = {"status": "success", "audited_at": datetime.now(), "updated_at": datetime.now()} if admin_notes: update_data["admin_notes"] = admin_notes await valuation.update_from_dict(update_data).save() out = ValuationAssessmentOut.model_validate(valuation) return await self._attach_user_phone(out) async def reject_valuation(self, valuation_id: int, admin_notes: Optional[str] = None) -> Optional[ValuationAssessmentOut]: """审核拒绝估值评估""" valuation = await self.model.filter(id=valuation_id, is_active=True).first() if not valuation: return None from datetime import datetime update_data = {"status": "rejected", "audited_at": datetime.now(), "updated_at": datetime.now()} if admin_notes: update_data["admin_notes"] = admin_notes await valuation.update_from_dict(update_data).save() out = ValuationAssessmentOut.model_validate(valuation) return await self._attach_user_phone(out) async def update_admin_notes(self, valuation_id: int, admin_notes: str) -> Optional[ValuationAssessmentOut]: """更新管理员备注""" valuation = await self.model.filter(id=valuation_id, is_active=True).first() if not valuation: return None from datetime import datetime await valuation.update_from_dict({"admin_notes": admin_notes, "updated_at": datetime.now()}).save() out = ValuationAssessmentOut.model_validate(valuation) return await self._attach_user_phone(out) async def update_calc(self, valuation_id: int, data: ValuationAssessmentUpdate) -> Optional[ValuationAssessmentOut]: valuation = await self.model.filter(id=valuation_id, is_active=True).first() if not valuation: return None update_data = data.model_dump(exclude_unset=True) valuation.status ="pending" if update_data: await valuation.update_from_dict(update_data) await valuation.save() out = ValuationAssessmentOut.model_validate(valuation) return await self._attach_user_phone(out) async def _attach_user_phone(self, out: ValuationAssessmentOut) -> ValuationAssessmentOut: user = await AppUser.filter(id=out.user_id).first() out.user_phone = getattr(user, "phone", None) if user else None return out async def _attach_user_phone_bulk(self, items: List[ValuationAssessmentOut]) -> List[ValuationAssessmentOut]: ids = list({item.user_id for item in items if item.user_id}) if not ids: return items users = await AppUser.filter(id__in=ids).values("id", "phone") phone_map = {u["id"]: u["phone"] for u in users} for item in items: item.user_phone = phone_map.get(item.user_id) return items # 创建控制器实例 valuation_controller = ValuationController() from app.log import logger