"""问卷服务:提交风险测评 → 完整性/选项校验 → 算分定级 → 落风评记录 → 回写画像。""" 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 utils.exceptions import NotFoundError, ParamError # 分数 → 风险等级映射(总分 0-100,20 分一档;后续可迁移到 sys_config 运营化) _SCORE_LEVELS = ( (80, "激进"), (60, "进取"), (40, "平衡"), (20, "稳健"), (0, "保守"), ) def _score_to_level(score: int) -> str: for threshold, level in _SCORE_LEVELS: if score >= threshold: return level return "保守" 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) return { "assessment_id": record.id, "total_score": total_score, "risk_level": risk_level, "valid_until": valid_until.isoformat(), }