From d8e47df20cac4fda3a37e307458ec4312ad7e900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=BF=E4=BA=91=E7=A7=8B=E6=9C=88?= <15273589815@163.com> Date: Mon, 14 Sep 2026 22:45:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A8=E4=BB=8B=E6=9D=90=E6=96=99=EF=BC=9A?= =?UTF-8?q?=E5=8B=BE=E9=80=89=E3=80=8C=E5=B1=95=E7=A4=BA=E6=8E=92=E5=90=8D?= =?UTF-8?q?=E3=80=8D=E5=90=8E=E6=97=A0=E6=B3=95=E5=A1=AB=E5=86=99=E6=9D=A5?= =?UTF-8?q?=E6=BA=90=20=E2=86=92=20=E5=BF=85=E7=84=B6=E5=90=88=E8=A7=84?= =?UTF-8?q?=E9=98=BB=E6=96=AD=EF=BC=88=E6=AD=BB=E9=94=81=EF=BC=89=E7=9A=84?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 现象(你的截图) 任务 `PM-20260914-0007` 的合规检查结果为: ``` 业绩排名来源不满足要求 补充三年期以上公开评价数据来源 · block ``` 输入快照里的实证: ```json ranking = {"enabled": true, "ranking_text": null, "public_source": null, "institution_name": null, "evaluation_period_years": null} ``` ## 根因:勾选框有了,填来源的地方没有 `promotion_compliance.py:82-91` 的规则是: ```python if ranking.get("enabled"): years = _years(ranking.get("evaluation_period_years")) if years < 3 or not ranking.get("institution_name") or not ranking.get("public_source"): → block "performance.ranking_source_invalid" ``` 而前端**只有**一个复选框 `performance_info.ranking.enabled`: - 表单里**没有** `institution_name` / `public_source` / `evaluation_period_years` / `ranking_text` 四个输入(实测枚举全部 `data-promo-field` 只有那一个 ranking 字段); - `inputsPayload()` 也只提交 `ranking: { enabled }`。 ⇒ 运营一旦勾选「展示排名」,来源四项永远是 null,规则必然阻断, 而界面上**无处可填** —— 要么取消勾选,要么永远生成不了。这是**死锁**,不是数据问题。 ## 修法(前后端契约字段本就有,只补界面与提交) 1. `promotion/index.html`:在展示选项上方新增一组输入(`data-ranking-source`): 评价期间(年,需 ≥3)/ 评价机构 / 公开来源 / 排名文本; 2. `promotion/promotion.js`:`inputsPayload()` 的 `ranking` 补上这四个字段 (契约 `RankingInfo` 本就定义了 `institution_name` / `evaluation_period_years` / `ranking_text` / `public_source`,`evaluation_period_years` 是字符串,如 "3年")。 ## 验证 - 用**真实任务 0007 的输入**跑 `PromotionComplianceChecker.check_inputs()`: - 现状:3 条阻断(`history_short` + **`ranking_source_invalid`** + `data_attachment_missing`); - 把来源填全后:**`ranking_source_invalid` 消失**(其余两条由"未上传业绩文件"引起,上传即解); - 反例:评价期间改成 2 年 → 仍拦;3 年但缺公开来源 → 仍拦(规则没有被放松); - 从源文件抽出 `inputsPayload()` 用桩真实调用:四个来源字段都进 payload;未勾选时 `enabled=false`; - `index.html` 标签净增 +1 `
` / +1 `
`(结构平衡);`node --check` 通过; - `pytest tests/unit tests/contract` → 1458 passed, 2 skipped, 0 failed。 ## 附带说明 你看到的是"2 条"是因为**点了两次生成**:每次生成都会把该次的 findings 落库, 页面"读取合规结果"会把历史记录一并列出(不是一次调用重复产出)。 --- app/static/portal/employee-operations/promotion/index.html | 2 +- app/static/portal/employee-operations/promotion/promotion.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/static/portal/employee-operations/promotion/index.html b/app/static/portal/employee-operations/promotion/index.html index 72a3230..a6ea9b7 100644 --- a/app/static/portal/employee-operations/promotion/index.html +++ b/app/static/portal/employee-operations/promotion/index.html @@ -29,7 +29,7 @@

产品信息

管理人信息

团队与策略

-

费用、业绩与风险

+

费用、业绩与风险

diff --git a/app/static/portal/employee-operations/promotion/promotion.js b/app/static/portal/employee-operations/promotion/promotion.js index 6e82847..d6cc091 100644 --- a/app/static/portal/employee-operations/promotion/promotion.js +++ b/app/static/portal/employee-operations/promotion/promotion.js @@ -131,7 +131,7 @@ if (requireOperator()) { team_info: { team_description: text('team_info.team_description'), research_capability: nullable('team_info.research_capability') }, strategy_info: { investment_scope: text('strategy_info.investment_scope'), strategy: text('strategy_info.strategy'), restrictions: text('strategy_info.restrictions'), index_tool_attribute: nullable('strategy_info.index_tool_attribute') }, fee_structure: { subscription_fee: nullable('fee_structure.subscription_fee'), purchase_fee: nullable('fee_structure.purchase_fee'), redemption_fee: nullable('fee_structure.redemption_fee'), sales_service_fee: nullable('fee_structure.sales_service_fee'), management_fee: nullable('fee_structure.management_fee'), custody_fee: nullable('fee_structure.custody_fee'), client_maintenance_fee: nullable('fee_structure.client_maintenance_fee') }, - performance_info: { as_of_date: nullable('performance_info.as_of_date'), history_months: Number(field('performance_info.history_months')?.value || 0) || null, product_return: nullable('performance_info.product_return'), max_drawdown: nullable('performance_info.max_drawdown'), volatility: nullable('performance_info.volatility'), sharpe_ratio: nullable('performance_info.sharpe_ratio'), show_product_performance: Boolean(field('performance_info.show_product_performance')?.checked), show_manager_performance: Boolean(field('performance_info.show_manager_performance')?.checked), ranking: { enabled: Boolean(field('performance_info.ranking.enabled')?.checked) } }, + performance_info: { as_of_date: nullable('performance_info.as_of_date'), history_months: Number(field('performance_info.history_months')?.value || 0) || null, product_return: nullable('performance_info.product_return'), max_drawdown: nullable('performance_info.max_drawdown'), volatility: nullable('performance_info.volatility'), sharpe_ratio: nullable('performance_info.sharpe_ratio'), show_product_performance: Boolean(field('performance_info.show_product_performance')?.checked), show_manager_performance: Boolean(field('performance_info.show_manager_performance')?.checked), ranking: { enabled: Boolean(field('performance_info.ranking.enabled')?.checked), institution_name: nullable('performance_info.ranking.institution_name'), evaluation_period_years: nullable('performance_info.ranking.evaluation_period_years'), ranking_text: nullable('performance_info.ranking.ranking_text'), public_source: nullable('performance_info.ranking.public_source') } }, risk_disclosure: { special_risks: text('risk_disclosure.special_risks').split('\n').map((item) => item.trim()).filter(Boolean), additional_notes: nullable('risk_disclosure.additional_notes') }, source_notes: {}, };