90 lines
3.1 KiB
Python
90 lines
3.1 KiB
Python
"""问卷服务:提交风险测评 → 完整性/选项校验 → 算分定级 → 落风评记录 → 回写画像。"""
|
|
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
|
|
from service.memory.composed_profile import invalidate_composed_profile
|
|
from utils.exceptions import NotFoundError, ParamError
|
|
|
|
# 分数 → 风险等级映射(总分 0-100,20 分一档;后续可迁移到 sys_config 运营化)
|
|
# 等级口径统一为 R1-R5:R1 最保守、R5 最激进,与产品 fin_product.risk_level 对齐
|
|
_SCORE_LEVELS = (
|
|
(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
|
|
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)
|
|
# 画像变更后失效最终画像缓存(失败静默,不影响问卷提交)
|
|
await invalidate_composed_profile(customer_id)
|
|
|
|
return {
|
|
"assessment_id": record.id,
|
|
"total_score": total_score,
|
|
"risk_level": risk_level,
|
|
"valid_until": valid_until.isoformat(),
|
|
}
|