Files
Mutual_Fund/service/questionnaire.py
T

90 lines
3.1 KiB
Python
Raw Normal View History

"""问卷服务:提交风险测评 → 完整性/选项校验 → 算分定级 → 落风评记录 → 回写画像。"""
from __future__ import annotations
from datetime import date, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from model.fin_risk_assessment import FinRiskAssessment
from repositories.questionnaire import QuestionRepo, QuestionnaireRepo
from repositories.risk_assessment import CustomerProfileRepo, RiskAssessmentRepo
2026-09-14 00:19:31 +08:00
from service.memory.composed_profile import invalidate_composed_profile
from utils.exceptions import NotFoundError, ParamError
# 分数 → 风险等级映射(总分 0-100,20 分一档;后续可迁移到 sys_config 运营化)
2026-09-14 00:19:31 +08:00
# 等级口径统一为 R1-R5:R1 最保守、R5 最激进,与产品 fin_product.risk_level 对齐
_SCORE_LEVELS = (
2026-09-14 00:19:31 +08:00
(80, "R5"),
(60, "R4"),
(40, "R3"),
(20, "R2"),
(0, "R1"),
)
def _score_to_level(score: int) -> str:
for threshold, level in _SCORE_LEVELS:
if score >= threshold:
return level
2026-09-14 00:19:31 +08:00
return "R1"
async def submit_assessment(
db: AsyncSession,
customer_id: int,
questionnaire_id: int,
answers: list[dict],
) -> dict:
questionnaire = await QuestionnaireRepo(db).get(questionnaire_id)
if questionnaire is None:
raise NotFoundError("问卷不存在")
if questionnaire.status != "启用":
raise ParamError("问卷未启用")
questions = await QuestionRepo(db).list_by_questionnaire(questionnaire_id)
if not questions:
raise ParamError("问卷尚未配置题目")
# 提交答案按 question_no(题号)建索引,校验必填完整性与选项合法性
by_no = {a["question_no"]: a["option"] for a in answers}
total_score = 0
detail = []
for q in questions:
option = by_no.get(q.question_no)
if option is None:
raise ParamError(f"第{q.question_no}题未作答")
scores = q.score_json or {}
if option not in scores:
raise ParamError(f"第{q.question_no}题选项非法: {option}")
score = int(scores[option])
total_score += score
detail.append({"q": q.question_no, "a": option, "score": score})
risk_level = _score_to_level(total_score)
today = date.today()
valid_until = today + timedelta(days=365) # 监管要求一年一评
record = FinRiskAssessment(
customer_id=customer_id,
assessment_date=today,
question_version=questionnaire.version,
total_score=total_score,
risk_level=risk_level,
answers=detail,
assessor_type="AI评估",
valid_until=valid_until,
)
record = await RiskAssessmentRepo(db).add(record)
# 回写画像:无画像初始化,有画像更新风险等级/评分并递增版本号
await CustomerProfileRepo(db).upsert_risk(customer_id, risk_level, total_score)
2026-09-14 00:19:31 +08:00
# 画像变更后失效最终画像缓存(失败静默,不影响问卷提交)
await invalidate_composed_profile(customer_id)
return {
"assessment_id": record.id,
"total_score": total_score,
"risk_level": risk_level,
"valid_until": valid_until.isoformat(),
}