Files
bid-doctor/docs/architecture/project-info-plan.md
YANG JIANKUAN ed6a2a907d
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled
feat(project): 项目基本信息修改和分包选择(FR-M2-03、04)
- 字段修改:人工值优先、撤销恢复 AI 值、按字段校验(金额可写「286万」,按元存)、
  冗余列同步、审计留痕、只读项目 409;关键日期显示改后的投标截止时间
- 分包:增删改、选包(至少一个)、一键「不分包」;删包时条目归为通用,删到一个不剩回到不分包
- 清单列表 lot_scope 默认只列通用 + 所投的包,响应带 hidden_count
- 检查记下当时用的项目依据(截止时间、限价、所投的包、法律体系):报告按它写,
  之后改了信息检查结果接口给 basis_changed,查标页提示重新检查
- 前端:概览标签页(项目要点、采购包、关键日期)、字段修改对话框、选包页和手工调整;
  解析完识别到多个包时进选包页;读标页清单范围和所属采购包;查标页写明查哪几个包
- 决策 PI1–PI7;方案和落地记录 docs/architecture/project-info-plan.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:46:07 +08:00

12 KiB
Raw Blame History

项目基本信息修改和分包选择实现方案 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、一键改回不分包。