# -*- coding: utf-8 -*- """人工裁决理由的码表与拼句 (2026-09-14 系统自决包, 台账 015)。纯逻辑, 零外部依赖, 可单测。 系统自决把绝大多数「该不该做」接了过去, 剩下真交人的那几张提议, 人裁决时从预设理由里选一条, 选「其他」再补一句 —— 不再每次手写一整段。这里放两张码表 (采纳 / 驳回) 与一个拼句函数, 把「决定 + 码 + 补充 + 自由文本」拼成落进评审账本的一句话, 并回出用到的码 (进硬数字备查)。 页面拿这两张表铺预设按钮 (GET /api/proposals/decide-reasons); 后端在裁决入口再校验一次 —— 选「其他」不填补充、或码与决定对不上, 一律回中文错误, 不落账 (与研判不可用交人同一层的严)。 旧的自由文本 reason 仍然接受 (向后兼容: 脚本、旧缓存页面照发照收)。 """ from __future__ import annotations # 驳回理由码 → 中文标签。最后一个是「其他」, 选它必须填补充。 DECLINE_REASONS = { "DECLINE_CONSENSUS_MISMATCH": "与研判结论不符", "DECLINE_TECH_WEAK": "技术面已转弱", "DECLINE_POSITION_FULL": "仓位已达上限", "DECLINE_SECTOR_CONCENTRATED": "行业集中度过高", "DECLINE_WAIT_PULLBACK": "等待回调再议", "DECLINE_MARKET_RISK": "大盘风险偏高", "DECLINE_OTHER": "其他", } # 采纳理由码 → 中文标签。最后一个是「其他」, 选它必须填补充。 ACCEPT_REASONS = { "ACCEPT_CONSENSUS": "合议放行,按系统建议执行", "ACCEPT_MANUAL_JUDGE": "人工判断机会成立", "ACCEPT_PER_COMMAND": "按既有命令执行", "ACCEPT_RISK_CONTROL": "风控需要", "ACCEPT_OTHER": "其他", } def reasons_for(decision) -> dict: """按决定取对应的码表。ACCEPTED 用采纳表, 其余 (DECLINED) 用驳回表。""" return ACCEPT_REASONS if str(decision or "").upper() == "ACCEPTED" else DECLINE_REASONS def compose(decision, reason_code=None, reason_note=None, reason=None): """把裁决理由拼成一句话。返回 (账本句, 用到的码或 None)。 decision: ACCEPTED / DECLINED, 决定用哪张码表。 reason_code: 预设理由码 (可空)。给了就必须落在对应码表里, 否则报错。 reason_note: 补充说明 (选「其他」时必填)。 reason: 旧的自由文本 (向后兼容)。只有它、没有码时, 原样当账本句。 码与自由文本都空 → 报错 (空理由不收)。码不在表里 / 选「其他」不填补充 → 报错。 错误一律是能直接给人看的中文 ValueError。 """ decision = str(decision or "").upper() table = reasons_for(decision) code = (reason_code or "").strip() or None note = (reason_note or "").strip() free = (reason or "").strip() label_side = "采纳" if decision == "ACCEPTED" else "驳回" if not code and not free: raise ValueError("采纳或驳回都要给理由:选一个预设理由,或写一句自由说明") if code: if code not in table: raise ValueError(f"理由码 {code} 不适用于本次{label_side}") if code.endswith("_OTHER") and not note: raise ValueError("选「其他」时必须填补充说明") sentence = table[code] + (f";补充:{note}" if note else "") return sentence[:500], code # 只有自由文本 (旧字段兼容): 原样当账本句, 不带码。 return free[:500], None