# 项目基本信息修改和分包选择实现方案 v0.1(2026-11-04) > 范围:FR-M2-03 项目基本信息「用户可以修改,修改留痕」,FR-M2-04 分包识别与选择(G3)。 > 依据:`03-functional-requirements.md`、`02-user-journeys.md` J1 第 4 步、决策 G3 / DM7、`data-model.md` §5.3、`openapi.yaml`(`/projects/{id}/fields`、`/lots`、`/bidding-lots`)、原型 `project.html` `lots.html`。 > 状态:PI1–PI7 已全部按建议确认(2026-11-04,`docs/decisions.md`)。 ## 0. 结论速览 - **为什么现在做**:查标的几条规则直接用项目信息。投标截止日决定证书过期(第 4 步刚做的资质匹配)和落款日期;所投的包决定查哪些清单项、和哪个限价比;法律体系决定用「投标无效」还是「否决投标」。这些都是模型抽的,现在抽错了用户改不了,查标结果就跟着错。国动是真实文件,不能指望抽得全对。 - **表和接口契约都已经有了**:`project_field` 分开存 AI 值和人工值,`lot` 有 `ai_values` 和 `edited_fields`;`openapi.yaml` 定义了字段修改、分包增删改和选包的接口。查标已经按「人工值优先」读字段、按 `is_bidding` 读所投的包。缺的是这些接口的实现和前端页面。 - **分包识别经常不对**(§1),所以「手工调整」和「这个项目不分包」都要一步就能做到,不能藏得太深。 - **检查要记下当时用的项目依据**:现在报告首页读的是导出时的项目信息,检查用的是检查时的值,中间改过就对不上(§5)。 ## 1. 现状 **后端**:解析时 `project/extracted.py` 把 13 个字段和分包写进项目。用户改过的字段不覆盖,补遗的新值放进待确认;第一次识别到分包时替换掉建项目时的默认包。查标读字段时人工值优先,读所投的包用 `bidding_lot_ids`(不含默认包;没有分包时为空,表示整个项目)。清单项有 `lot_id`(空 = 各包通用),清单项修改接口已经能改 `lot_id`。 **没做的**:字段修改接口、分包增删改和选包接口、清单列表的 `lot_scope` 筛选;前端没有概览页、没有选包页,字段只在解析页显示了「识别到 N 个采购包」。 **分包识别的实际情况**(S01–S10 读标结果 `evals/out/runs/fix3/*/info.json`,对照 `07-sample-findings.md` 的人工档案): | 招标文件 | 档案 | 识别结果 | 问题 | |---|---|---|---| | S10 | 3 个采购包,各有限价 | 3 个包,名称、限价都对 | — | | S06 | 不分包(有分项限价) | 5 个「包」 | 把分项当成了包 | | S01、S08 | 不分包 | 各 1 个包(「01」「1」),带限价 | 单包项目写了包号 | | 其余 6 份 | 不分包 | 没有包 | — | 结论:只识别到 1 个包时按不分包处理(PI4);S06 这种错误要靠用户一键改成「不分包」,原属这些包的条目归为通用(PI2、PI3)。改进分包抽取属于读标专项优化(`extract-status.md`),这次不做,记进已知问题。 ## 2. 基本信息修改(FR-M2-03) ### 2.1 接口 按契约实现 `PUT /projects/{id}/fields/{field_key}`,`value = null` 表示撤销人工值、恢复 AI 值。 - **校验按字段**: | 字段 | 值 | 校验 | |---|---|---| | 名称、编号、采购人、代理机构、采购方式、评标方法 | 文本 | 去首尾空白;名称不能为空,其余空串等于撤销 | | 法律体系 | `gov_procurement` / `tendering` / `unknown` | 枚举 | | 预算、总价限价 | 金额字符串(元,两位小数) | 非负;界面可以写「286 万」「2,860,000」,提交前换成元(PI7) | | 联合体、进口产品、专门面向中小企业 | 布尔 | 也可以撤销成「没写明」 | | 投标截止时间 | 带时区的时间 | 界面按北京时间录入到分钟 | - 写 `human_value / human_set_by / human_set_at`;`project` 表上冗余的几列(名称、编号、采购人、代理机构、采购方式、法律体系、截止时间)同步更新,撤销时回到 AI 值。 - 每次修改写审计 `project_field.set`(前后值),字段的修改记录从审计里读,和清单项同一做法。 - 项目已递交或已归档时返回 409 `project_read_only`。`If-Match` 选填,和清单项修改一致(MVP 只有一个人用,先不强制)。 - 响应里的 `updated_by` 填人工修改人。 - **关键日期跟着改**:概览的关键日期来自「日期与递交」类清单项;投标截止时间被人工改过时,时间线上的「投标截止」显示改后的值并标「已修改」,不然两处对不上。 ### 2.2 不在这一步做的 补遗带来的待确认值(`POST .../fields/{key}/pending`)属于 FR-M2-10 补遗追加,下一步一起做;这次只在字段上显示「补遗 N 带来了新值」,不能确认。 ## 3. 分包(FR-M2-04) ### 3.1 接口 按契约实现分包列表、新增、修改、删除和选包。补充几条规则: - **默认包**:没分包的项目有一个默认包(`is_default`),界面上不显示,所有条目都归它(`lot_id` 为空)。手工新增第一个包时删掉默认包;删掉最后一个真实的包时自动建回默认包,即回到「不分包」,**不报 `last_lot`**(PI2,改契约)。 - **「这个项目不分包」**:新增 `POST /projects/{id}/lots/clear`,一次删掉所有包、建回默认包,条目都归为通用(PI2)。S06 那样识别错的项目一步改好。 - **删除一个包**:原属这个包的清单项归为各包通用(契约原文,外键 `ON DELETE SET NULL`);确认框写明「有 N 条清单项会归为通用」(PI3)。 - **修改**:包号(项目内不能重复)、名称、预算、限价;AI 识别的包改过的字段记进 `edited_fields`,AI 原值留在 `ai_values`。 - **选包**:`PUT /projects/{id}/bidding-lots`,至少选一个。没选过时所有包都算投(表的默认值),选包页会提示先选。 - 增删改、选包都写审计(`lot.create / update / delete / clear`、`project.bidding_lots`);只读项目返回 409。 ### 3.2 清单按包筛选 - 清单列表加 `lot_scope`:`bidding`(默认,通用 + 所投的包)/ `all`。响应里带被隐藏的条数,清单页顶部写「通用 + 采购包 1 · 隐藏 23 条」,可以切到全部。 - 清单项对话框加「所属采购包」下拉(通用 / 各包),改归属走现有的清单项修改接口,留修改记录。 - 换了所投的包,清单的确认状态不变:清单内容没变,变的只是查哪些(PI6)。查标本来就按检查时的所投的包筛选,不在所投包里的条目写进覆盖范围。 ## 4. 前端 - **概览标签页**(原型 `project.html`):这一步只做三张卡(PI1): - **项目要点**:13 个字段,每个带出处(点开看原文);人工改过的标「已修改」,可以恢复 AI 值;点「修改」打开对话框,按字段类型给输入控件。 - **采购包**:包号、名称、预算、限价、是否投;「更改所投的包」去选包页。不分包的项目这张卡写「不分包」。 - **关键日期**:时间线和倒计时(FR-M2-09)。 - 原型里的检查记录、文件、AI 摘要卡留到第 6 步。 - **选包页**(原型 `lots.html`): - 解析完成后,识别到 2 个及以上的包时,解析页的「下一步」进选包页,否则直接进清单(PI4)。 - 页面内容:每个包一张选择卡(包号、名称、预算、限价带出处、各包的条目数)、通用条目数、「识别得不对?手工调整」、「这个项目不分包」。 - 手工调整对话框:增删包、改包号、名称、预算、限价。 - **读标页**:顶部显示「通用 + 采购包 1」和隐藏条数,带「更改所投的包」链接;清单项对话框加所属包。 - **查标页**:检查预览里写明这次查哪几个包。检查结果页在项目信息于检查之后改过时提示「项目信息在本次检查后改过,重新检查后生效」(PI5)。 - 金额输入和显示:界面写元或万元都行,显示「286.00 万元」;限价旁边注明「招标文件原文」(红线 6:不是报价建议)。 ## 5. 检查记下当时的项目依据(PI5) `check_run` 已经存了 `lot_ids`。再在覆盖范围里存一份 `basis`:投标截止时间、总价限价、所投各包的包号和限价、法律体系,各带来源(AI / 人工)。 - **报告首页**改为读 `basis`。用户在检查后改了截止日再导出报告时,报告写的是检查时用的值,和问题明细一致。 - **查标结果页**比较 `basis` 和项目当前的值,不一样就出上面那条提示。 - 不加迁移,`coverage` 是 jsonb。 ## 6. 测试 - 集成测试:字段修改、撤销、校验、只读、审计、冗余列同步;分包增删改、默认包的建回、「不分包」、删包时条目归通用、选包至少一个;清单 `lot_scope`;检查用了改后的截止日和所投的包,`basis` 记下的是检查时的值。 - 前端在浏览器里走一遍:概览改截止日 → 查标页提示重新检查;选包页手工调整、一键不分包、选包后清单隐藏条数。 - 不需要真实模型:分包用接口直接建,字段用测试样例解析出来的。 ## 7. 工作量和顺序 1. 后端字段修改、分包接口、`lot_scope`、`basis`,以及测试。约半天。 2. 前端概览页(三张卡和修改对话框)。约半天。 3. 前端选包页、读标页和查标页的联动。约半天。 ## 8. 待确认事项 | 编号 | 事项 | 建议 | |---|---|---| | PI1 | 字段在哪里改 | **做「概览」标签页**,这一步只做项目要点、采购包、关键日期三张卡;检查记录、文件、AI 摘要卡留到第 6 步。另一种做法是只在项目头放一个「项目信息」对话框,省事,但以后还得做概览页 | | PI2 | 删掉最后一个包 | **回到「不分包」**(自动建回默认包),不报 `last_lot`;另给「这个项目不分包」一键操作。S06 那种把分项识别成 5 个包的,一步改好 | | PI3 | 删包时原属这个包的条目 | **归为各包通用**(契约原文),确认框写明条数。被误识别的包里的条目多半本来就是通用的;归通用宁可多查,不会漏查 | | PI4 | 只识别到 1 个包 | **按不分包处理**:跳过选包页,这个包的限价照用(作为采购包限价)。S01、S08 都是这种情况 | | PI5 | 检查之后改了项目信息 | **检查记下当时的依据**,报告按检查时的值写;查标页提示「重新检查后生效」。不自动重新检查(重新检查可能扣点,红线 9 要求用户自己决定) | | PI6 | 换了所投的包,清单还算不算已确认 | **算**:清单内容没变;查标按检查时的所投的包筛选,覆盖范围里写明不在所投包里的条数 | | PI7 | 金额怎么录 | **按元存**,界面可以写「286 万」「2,860,000」,提交前换成元;显示用万元 | ## 9. 落地记录(2026-11-04) - 后端:`project/edit.py`(字段修改、分包增删改、「不分包」、选包,都写审计);`project/service.basis` 是检查时的项目依据,查标存进覆盖范围、检查结果接口给 `basis_changed`,报告首页按它写截止时间;清单列表 `lot_scope` 默认只列通用 + 所投的包,响应带 `hidden_count`。 - 名称没有 AI 值时(建项目时手填的),撤销人工值保持现在的名称;界面只在有 AI 值时给「恢复 AI 值」。 - 前端:概览页三张卡(项目要点的值没有字段记录时取项目本身的值,比如建项目时填的名称)、字段修改对话框、选包页和手工调整对话框;解析完识别到 2 个及以上的包时「下一步」进选包页;读标页显示清单范围和隐藏条数,可以「显示全部」;清单项对话框加「所属采购包」;查标页写明本次查哪几个包;检查结果页在 `basis_changed` 时提示重新检查。 - 浏览器里走过:改截止时间(项目头、要点、关键日期一起变)、写「286万」的限价、手工加两个包并写限价、只投包 1、一键改回不分包。