Files

263 lines
9.3 KiB
Python
Raw Permalink 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.
"""主审员画像库
功能:
1. 存储主审员基础信息(专长、效率、质量评分)
2. 动态更新主审员负荷
3. 多维度评分(用于智能分配)
"""
import json
from typing import Dict, List, Optional
from pathlib import Path
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class ReviewerProfileStore:
"""主审员画像存储"""
def __init__(self, data_dir: str = "./xingbang_data/reviewers"):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
self.profiles_file = self.data_dir / "reviewer_profiles.json"
# 初始化默认数据
if not self.profiles_file.exists():
self._init_default_profiles()
def _init_default_profiles(self):
"""初始化默认主审员画像"""
default_profiles = {
"李明": {
"name": "李明",
"specialty": ["制造业", "新能源"],
"current_projects": 5,
"avg_completion_days": 8,
"quality_score": 0.85, # 0-1,基于历史评审质量
"efficiency_score": 0.78, # 0-1,基于完成速度
"workload_capacity": 7, # 最大承载项目数
"industry_experience": {
"制造业": 45, # 历史评审项目数
"新能源": 32,
"光伏": 18,
},
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
},
"王芳": {
"name": "王芳",
"specialty": ["新能源", "光伏", "风电"],
"current_projects": 3,
"avg_completion_days": 7,
"quality_score": 0.92,
"efficiency_score": 0.88,
"workload_capacity": 6,
"industry_experience": {
"新能源": 56,
"光伏": 38,
"风电": 22,
},
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
},
"张强": {
"name": "张强",
"specialty": ["交通运输", "物流", "制造业"],
"current_projects": 7,
"avg_completion_days": 9,
"quality_score": 0.80,
"efficiency_score": 0.72,
"workload_capacity": 8,
"industry_experience": {
"交通运输": 67,
"物流": 43,
"制造业": 28,
},
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
},
"刘洋": {
"name": "刘洋",
"specialty": ["医疗健康", "服务业", "消费"],
"current_projects": 4,
"avg_completion_days": 8,
"quality_score": 0.88,
"efficiency_score": 0.82,
"workload_capacity": 7,
"industry_experience": {
"医疗健康": 51,
"服务业": 34,
"消费": 29,
},
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
},
}
with open(self.profiles_file, "w", encoding="utf-8") as f:
json.dump(default_profiles, f, ensure_ascii=False, indent=2)
logger.info("主审员画像初始化完成")
def get_all_profiles(self) -> Dict[str, Dict]:
"""获取所有主审员画像"""
with open(self.profiles_file, "r", encoding="utf-8") as f:
return json.load(f)
def get_profile(self, reviewer_name: str) -> Optional[Dict]:
"""获取指定主审员画像"""
profiles = self.get_all_profiles()
return profiles.get(reviewer_name)
def update_workload(self, reviewer_name: str, current_projects: int):
"""更新主审员工作负荷"""
profiles = self.get_all_profiles()
if reviewer_name in profiles:
profiles[reviewer_name]["current_projects"] = current_projects
profiles[reviewer_name]["updated_at"] = datetime.now().isoformat()
with open(self.profiles_file, "w", encoding="utf-8") as f:
json.dump(profiles, f, ensure_ascii=False, indent=2)
logger.info(f"已更新 {reviewer_name} 的负荷: {current_projects} 个项目")
def update_quality_score(self, reviewer_name: str, quality_score: float):
"""更新主审员质量评分"""
profiles = self.get_all_profiles()
if reviewer_name in profiles:
profiles[reviewer_name]["quality_score"] = quality_score
profiles[reviewer_name]["updated_at"] = datetime.now().isoformat()
with open(self.profiles_file, "w", encoding="utf-8") as f:
json.dump(profiles, f, ensure_ascii=False, indent=2)
logger.info(f"已更新 {reviewer_name} 的质量评分: {quality_score:.2f}")
def calculate_match_score(
self,
reviewer_name: str,
project_industry: str,
project_complexity: int = 3
) -> Dict:
"""计算主审员与项目的匹配度(多维度评分)
评分维度:
- 35% 行业匹配度(专长+历史经验)
- 35% 可用性(当前负荷)
- 20% 质量评分(历史评审质量)
- 10% 团队平衡(避免某人长期超载)
"""
profile = self.get_profile(reviewer_name)
if not profile:
return {"total_score": 0, "breakdown": {}}
# 1. 行业匹配度(35%)
industry_match = 0
if any(spec in project_industry or project_industry in spec for spec in profile["specialty"]):
industry_match = 0.8 # 专长匹配
# 检查历史经验
exp_count = 0
for industry_key in profile["industry_experience"]:
if industry_key in project_industry or project_industry in industry_key:
exp_count = profile["industry_experience"][industry_key]
break
if exp_count >= 30:
industry_match = 1.0
elif exp_count >= 10:
industry_match = max(industry_match, 0.6)
elif exp_count > 0:
industry_match = max(industry_match, 0.3)
industry_score = industry_match * 35
# 2. 可用性(35%)
current_load = profile["current_projects"]
capacity = profile["workload_capacity"]
if current_load >= capacity:
availability = 0.1 # 超载
elif current_load >= capacity * 0.8:
availability = 0.5 # 偏高
else:
availability = 1.0 - (current_load / capacity) # 线性递减
availability_score = availability * 35
# 3. 质量评分(20%)
quality_score = profile["quality_score"] * 20
# 4. 团队平衡(10%)- 优先分配给负荷较低的
all_profiles = self.get_all_profiles()
avg_load = sum(p["current_projects"] for p in all_profiles.values()) / len(all_profiles)
if current_load < avg_load:
balance_score = 10
elif current_load == avg_load:
balance_score = 7
else:
balance_score = max(0, 10 - (current_load - avg_load) * 2)
# 总分
total_score = industry_score + availability_score + quality_score + balance_score
return {
"total_score": round(total_score, 2),
"breakdown": {
"industry_match": round(industry_score, 2),
"availability": round(availability_score, 2),
"quality": round(quality_score, 2),
"balance": round(balance_score, 2),
},
"estimated_days": profile["avg_completion_days"] + current_load // 2, # 考虑负荷影响
}
def get_recommended_reviewers(
self,
project_industry: str,
project_complexity: int = 3,
top_n: int = 3
) -> List[Dict]:
"""获取推荐主审员列表(Top N)"""
all_profiles = self.get_all_profiles()
recommendations = []
for reviewer_name in all_profiles:
match_result = self.calculate_match_score(
reviewer_name,
project_industry,
project_complexity
)
profile = all_profiles[reviewer_name]
recommendations.append({
"reviewer": reviewer_name,
"score": match_result["total_score"],
"breakdown": match_result["breakdown"],
"estimated_days": match_result["estimated_days"],
"current_projects": profile["current_projects"],
"specialty": profile["specialty"],
})
# 按总分降序排序
recommendations.sort(key=lambda x: x["score"], reverse=True)
return recommendations[:top_n]
# 全局单例
_reviewer_store: Optional[ReviewerProfileStore] = None
def get_reviewer_store() -> ReviewerProfileStore:
"""获取主审员画像存储单例"""
global _reviewer_store
if _reviewer_store is None:
_reviewer_store = ReviewerProfileStore()
return _reviewer_store