Files

93 lines
5.7 KiB
JSON
Raw Permalink Normal View History

{
"evidence_id": "20260919-t7c-profile-tier",
"generated_at": "2026-09-19T17:29:52+08:00",
"session": "D1.6 §4.32 / D2.1 v6.19",
"what": "D-11 落地:把「客户分层」接通(sys_user.customer_tier -> 画像快照 -> 客服作答),过程中定位并修掉真正的根因 —— 画像快照被残片覆盖 + JSON 列二次编码",
"how": "连库取证 profile_snapshots 全版本 -> 对读两个快照写者的产出形状 -> 修 -> pytest / 冒烟 / 真 HTTP 复验",
"corrected_diagnosis": {
"previous": "D-11 原判:sys_user.customer_tier 全库 NULL(从未种过)+ 画像快照按设计排除该字段",
"actual": "主因是两个写者互相覆盖:ProfileAssemblyService.rebuild_profile() 就地拼一个只含 PROFILE_OWNED_FIELDS + generated_at 的四字段残片,同样置 is_current=1,把权威快照里的 customer_tier / total_asset / behavior_score / assessment_* 全部抹掉。实测客户 9001 当前快照被覆盖到只剩 {risk_tags, generated_at, investor_type, investment_horizon, preferred_asset_class}(版本 2..16 全是这种形状)。「字段没种」与「写侧排除」两条也都成立,但都是次因。",
"evidence_query": "SELECT version, is_current, snapshot FROM profile_snapshots WHERE customer_id=9001"
},
"fixes": [
{
"id": "D-12a",
"file": "app/repository/profile_repository.py",
"change": "_SQL_PROFILE 增加 LEFT JOIN sys_user 取 customer_tier(fin_customer_profile 无该列;用 LEFT JOIN 使缺账号行时画像仍能生成)"
},
{
"id": "D-12b",
"file": "app/service/profile_generation_service.py",
"change": "build_snapshot 纳入 customer_tier;REQUIRED_SNAPSHOT_FIELDS 取消 customer_tier 例外(写侧与读取侧白名单必须等集)"
},
{
"id": "D-12c",
"file": "app/service/profile_assembly_service.py",
"change": "rebuild_profile 改走 build_snapshot()(唯一实现),不再自拼残片;字段所有权约束的是「写 fin_customer_profile 表」,不是快照字段集合"
},
{
"id": "D-13",
"file": "app/service/profile_assembly_service.py",
"change": "新增 PROFILE_JSON_FIELDS:preferred_asset_class / risk_tags 是 JSON 列,不再套 _as_text(会把列表序列化成字符串再被 JSON 列二次编码);risk_tags 改存数组"
},
{
"id": "D-13b",
"file": "app/service/profile_generation_service.py",
"change": "_as_list 兜底 JSON 字面量 \"null\"(当作空),并保留 json.loads 已脱引号的值"
},
{
"id": "D-14",
"file": "tools/seed_profile_demo.py",
"change": "① 客户分层写进权威列 sys_user.customer_tier;② 新增 user_facts 种子(preference:horizon / preference:asset_class)—— 这两个字段由重建服务独占、无事实即清空,写主表列会被下一次重建抹掉;③ 快照改由 ProfileGenerationService.generate() 生成,不再手拼"
}
],
"before_after": {
"snapshot_9001_before": {
"investor_type": "C1",
"risk_tags": null,
"generated_at": "...",
"investment_horizon": null,
"preferred_asset_class": null
},
"snapshot_9001_after": {
"risk_tags": [],
"total_asset": "60000.00",
"customer_tier": "gold",
"investor_type": "C1",
"behavior_score": 22,
"last_active_at": "2026-09-16T09:27:34+00:00",
"trading_frequency": "low",
"assessment_expired": false,
"investment_horizon": "short_term",
"preferred_asset_class": [
"money_fund"
],
"assessment_valid_until": "2027-07-16T09:27:34+00:00"
},
"answer_before": "您的风险测评等级是 保守型(C1)。",
"answer_after": "您的风险测评等级是 保守型(C1)。 / 投资期限偏好:短期(1 年以内)。 / 交易频率:较低。 / 偏好资产类别:货币基金。 / 客户分层:金卡。"
},
"authority_layout": {
"customer_tier": "sys_user.customer_tier(权威列,本次补种 6 行)",
"user_facts": "preference:horizon / preference:asset_class(12 行 = 6 客户 x 2)",
"fin_customer_profile": "investor_type / investment_horizon / preferred_asset_class / trading_frequency / total_asset / behavior_score / risk_tags",
"note": "investment_horizon / preferred_asset_class 由 ProfileAssemblyService 独占,无 user_facts 即清空 —— 所以种子的权威来源是 user_facts,不是主表列"
},
"gates": {
"pytest": "1746 passed / 0 failed / 2 skipped(较上一轮 +4 条新用例)",
"ruff": "22(= 基线)",
"mypy": "3(= 基线)",
"smoke": "tools/e2e_smoke_test.py --read-only -> 31/31 全通过",
"http_probe": "_eval_harness/http_probe.py -> 11/11 全 succeeded"
},
"tests_added": [
"tests/unit/service/test_profile_generation_service.py::test_snapshot_carries_customer_tier_from_its_authority",
"tests/unit/service/test_profile_generation_service.py::test_json_null_literal_is_not_a_preference",
"tests/unit/service/test_profile_generation_service.py::test_json_string_value_loses_its_serialization_quotes",
"tests/integration/test_profile_snapshot_completeness_mysql.py::test_rebuild_writes_a_complete_snapshot_not_a_partial_fragment(含 JSON 列二次编码断言)"
],
"open_items": [
"分层档位与 total_asset 不一致:6 行里 5 行的 tier 与公开门槛(金卡 50 万+/白金 200 万+/钻石 600 万+/尊享 1000 万+)对不上;但 agent 只读 sys_user.customer_tier、不用资产推,因此回答本身不自相矛盾 —— 口径待定",
"risk_tags 是「自述事实重算」字段:种子写进列的标签在首次记忆重建后会被清空(9001 实测由 [conservative] 变 []);要演示标签需种 user_facts 自述项"
]
}