销售框架合同信息查询 · 规范(三源合并版)

由三条相互独立的 SDD(Spec-Driven Development)路径对同一份需求、同一份老系统代码分别产出规范后合并而成。 本文档是开发评审用的单一事实来源:共识项直接采纳,分歧项已给出明确裁决并标注影响面, 待业务确认项集中列在第 7 章,可打印签字。

版本 v1.0(合并稿) 日期 2026-10-09 需求来源 2.销售合同管理-框架合同信息查询功能设计说明(确认版) 事实来源 onesystem/old-src/(只读) 落地目标 onesystem/app/(Next.js + Drizzle) 状态 待开发评审 + 待 4 项业务确认

01摘要与三源合并

一句话:这是在既有销售合同单表上新建的一个只读分组查询——按「框架合同 → 其下订单合同」成组展示,7 个筛选、16 个展示字段、4 条处理规则、2 个权限点;数据面无新表,全部靠新增字段与自关联表达。

需求要素
7 筛选 · 16 展示
+ 4 处理规则 · 2 权限点
展示字段来源
8 复用
5 新增 · 1 派生 · 1 前端 · 1 待确认
新增列
8
全部允许为空,由导入侧写入
待业务确认
4
见第 7 章,均为口径类
A · mattpocock/skills 30 条 user story|7 段模板 拷问决策台账(16 个未定义点) B · OpenSpec 26 Requirement|48 Scenario 机检护栏 validate --strict C · spec-kit 35 FR|44 任务|5 条宪法原则 spec ↔ plan ↔ tasks 交叉检查 合并规范 v1.0(本文档) 共识 6 项 → 直接采纳 分歧 4 项 → 明确裁决 + 标注影响面 A 路遗漏 1 项 → 本次补齐
图 1|三条独立路径产出三份规范,合并时不做「取并集」——而是先判「共识 / 分歧 / 缺项」,再对分歧逐条裁决。

本文档怎么用

开发人员

第 4 章是唯一实现依据;第 5 章给任务与文件落点;第 6 章给验收口径。第 2 章可不读。

业务 / 产品

只需读第 7 章(4 项确认单)+ 第 2.2 节(分歧影响面)。

评委 / 架构

读第 2 章(三路异同)与第 8 章(工程约束),验证合并过程是否可复核。

开工前置(硬阻塞) 本功能**没有数据可查**,除非姊妹需求《1.销售合同管理-分公司合同信息导入》先落地:框架类型、框架期限、各类预估额、订单指向框架的关联列,全部由导入侧写入。该需求文档未在本次输入中提供——这是三路一致标注的唯一硬阻塞项(见 §5 切片 S1 与 §7 确认项 C-04)。

02三路规范异同

三路在互不可见的前提下独立分析同一份需求。结果是:需求要素 26/26 三路全覆盖(覆盖完整性不是差异点);真正的差异出现在「需求没写、但实现必须拍板」的地方。

A · mattpocock
26/26
11 个产物 · 16 个未定义点
B · OpenSpec
26/26
5 个产物 · 9 个未定义点
C · spec-kit
26/26
12 个产物 · 16 个未定义点
三路一致项
6
+ 完全一致的 7 项老系统差异

2.1 共识清单(直接采纳)

以下 6 项是三路在互不知情的情况下给出同一答案的决策。这类结论可信度最高——它不依赖任何单一路径的判断,直接进入规范正文。

#议题三路一致的结论来源
1状态可见口径只展示合同审核状态为「已确认」(applyStatus='yqr')的合同;框架合同自身亦须已确认——沿用老系统台账口径,保证两系统数字可比A · B · C
2分页单位以框架合同为分页单位:先分页框架,再按本页框架批量取订单组装成组;totalRows 只用于展示、不参与分页计算A · B · C
3关联键与唯一性关联键 = 框架合同的销售合同编号(订单侧存该编号);因库中无唯一索引,须「写入侧前置唯一性校验 + 查询侧去重兜底」双保险A · B · C
4期限交集端点闭区间相交,含端点(查询区间端点恰等于框架期限起/止当天即命中)A · B · C
5「合同类型2」列新增独立列,枚举 framework / order,不得复用既有「合同类型(C)」(老库已有 CONTRACTTYPE NVARCHAR2(1),撞名风险)A · B · C
6序号语义仅框架行显示序号,订单行不显示;序号按结果集跨页全局连续,不按页重排A · B · C

2.1.1 三路一致认定的老系统差异(同一份事实)

三路各自独立取证,对老系统现状的 7 项判断完全一致(细节与出处见第 3 章)。这说明事实层的分歧风险很低——合并时不需要重新裁决现状。

#项老系统现状需求写法
D1数据可见口径主 SQL 硬过滤 applyStatus='yqr'(ContractSellQueryMapper.xml:124)需求未提 → 采纳「只出已确认」
D2排序键order by a.oid desc(:205)——写入主键倒序,与签订时间无关按框架签订时间倒序、空值靠后 → 必改
D3部门权限判定字段执行部门 OR 签订部门(ContractSellQueryController.java:104-105;SQL :144-146)仅「签订部门」→ 见 §2.2 裁决 1
D4模糊搜索范围3 列:name / contractId / economicsLawNumber(:126)4 列(+ 分公司合同编号)→ 需扩列
D5框架/订单与预估额字段销售侧完全不存在(无表、无列、无 domain、无代码)需求大量引用 → 需新建列 + 强依赖导入侧
D6关联键唯一性ECONOMICSLAWNUMBER 无唯一索引(唯一索引仅 OID、CONTRACTID)以销售合同编号关联 → 行放大风险
D7功能位置【合同台账查询(分公司)】菜单名在仓库中未找到(菜单配置在数据库)「在其下方新增」→ 落点需业务确认

2.2 分歧与裁决

4 项分歧 + 1 项遗漏。每项都给出裁决、理由、以及「如果你不同意会怎样」的影响面。争议项强制标注为待业务确认,但不阻塞开发——开发按裁决值先行。

分歧 1部门级权限按哪个字段判定?需业务确认

需求原文:「部门级可查看『签订部门』为本部门的框架合同及其下属订单合同」——只提签订部门。老系统台账查询用的是「执行部门 OR 签订部门」双字段。

A 路主张沿用老系统「执行部门 OR 签订部门」。理由:避免事业部用户可见数据收缩、防止投诉。
B 路 / C 路主张 · 本文采纳只认签订部门,照需求字面实现。
裁决 采用「只认签订部门」,并在第 7 章列为确认项 C-01。
理由:① 这是一个全新功能,不是改造既有台账查询——不存在「可见数据回退」的问题,A 路的投诉论据在这里不成立;② 需求是已由营销中心与分公司签批的确认版,字面效力最高;③ 「签订部门」是业务上定义框架合同归属的字段,与「本部门的框架合同」这一表述自洽。
影响面:部门级用户看到的框架范围比老系统台账口径窄。若业务实际期望「执行部门也进范围」,改动点集中在一处权限条件(OR 追加一项),成本可控。
分歧 2权限裁剪粒度:框架属于本部门、其订单属于他部门时怎么办?需业务确认

需求未定义。这是「组级权限」与「行级权限」的取舍。

A 路 / C 路主张 · 本文采纳组级:框架行按部门判定;一旦框架命中本部门,其下全部订单一并展示(组内不裁剪)。
B 路主张按签订部门裁剪订单——他部门订单不展示。
裁决 采用组级权限,并列为确认项 C-02。
理由:① 需求的可见对象是「本部门的框架合同及其下属订单合同」,是组级实体;② B 路的行级裁剪会造成「框架显示为 3 个订单、实际只露出 1 个」的静默缺失,直接破坏本功能最核心的「一屏看完整组」价值;③ 与「分页以框架为单位、组不撕裂」的共识(§2.1 第 2 项)保持同一套分组语义。
影响面:部门级用户可能看到他部门的订单行(作为本部门框架的子行)。若业务不接受,须改为行级过滤,并同步重新定义分组呈现与分页语义——这是本裁决唯一的实质代价。
分歧 3孤儿订单(订单指向的框架合同不存在)如何处理?需业务确认

需求未定义。三路给出三种不同态度,其中 A 路完全未意识到该场景。

C 路主张 · 本文采纳(加硬化)不展示于分组列表(无父分组,强行展示会破坏「分组 = 框架」语义与分页单位);但必须产生可观测告警日志,并可导入侧前置拦截。
B 路主张保留为独立行并标注「框架缺失」,不静默丢弃。
A 路未处理——grep 全篇无「悬空 / 孤儿 / 框架缺失」相关条目。
裁决 不展示 + 告警 + 写入侧拦截(三层)。
理由:列表的唯一分组单位是框架合同;无父的订单既无法归组,也会让「分页以框架为单位」的计数失真。但「静默丢弃」是不可接受的——所以补齐 C 路未强调的两层:可观测告警日志(运维可发现)+ 导入侧父子完整性校验(从源头拦住)。
影响面:异常数据不出现在业务视图,靠日志与导入校验闭环。若业务希望「在界面上就能看到未归属订单」,则改采 B 路方案:在列表末尾追加「未归属订单」伪分组,需同步调整分页与序号口径。
分歧 4「导入日期」新增列后,历史数据怎么回填?需业务确认

三路一致同意「新增独立列、不复用创建时间」,但对历史数据的取值出现细微分歧。

C 路主张 · 本文采纳历史非导入数据该字段留空;按「导入日期」筛选时空值被排除。
B 路主张老数据回填为 createTime。
裁决 历史数据留空,不回填,并列为确认项 C-03。
理由:回填 createTime 会伪造语义——手工录入的合同同样有 createTime,回填后会被当作「被导入过」的合同,使「导入日期」这个筛选条件从第一天起就撒谎。留空 + 明确排除,是唯一不会污染口径的选择。
影响面:历史合同按导入日期筛选时查不到(符合事实:它们确实不是导入进来的)。若业务确实需要区分「导入 / 手工录入」,可另用 dataType='import' 标记做辅助判断,不冲突。
本次补齐A 路遗漏:孤儿订单场景已修复

A 路用「设计树 + 两轮共 16 个拷问」产出最多未定义点,却漏掉了「订单指向的框架不存在」这一边界。B、C 两路都识别到了,并给出了相反的处理策略。这提示:流程本身不保证想到边界情况——本规范在 §4.5 把该场景写成显式规则(R-05),并要求配套告警与导入校验。

2.3 三路方法论差异(与规范内容无关,供选型参考)

维度A · mattpocockB · OpenSpecC · spec-kit
心智模型一组可组合的小技能specs/=现状,changes/=提案(delta)规格即源码,阶段流水线
流程刚性最松中最紧(七步 + 宪法门禁)
产物主形态拷问决策台账 + tracer 票据Requirement / ScenarioFR + plan + tasks + 契约
独有能力设计树拷问、每问带推荐答案、阻塞边票据图validate --strict 机检护栏宪法门禁 + spec↔plan↔tasks 三方交叉检查
本次实测短板漏了孤儿订单场景;技能假设有真实 tracker 与在线用户表单类需求上未新增结构化信息密度对高密度单页表单需求,user story 粒度偏粗(被硬拆成 4 个故事)
证据穷尽性⭐⭐⭐ 找到完整 DDL,并纠正既有分析的两处计数错误⭐⭐⭐ 同左,并发现被引用的文件实际不存在⭐⭐ 未定位到 DDL 来源,把可查证事实记为「未找到」

注:证据穷尽性是本次三路唯一出现“客观正确性差异”的维度——A、B 两路都挖到了 onesystem/analysis/ddl/*.txt(由 old-src/oracle.dmp 解析出的权威 DDL),C 路停在「源码里没有 → 未找到」。

03老系统现状取证

三路一致认定的事实,逐条带 文件:行号。查不到依据的一律显式写「未找到」,不用常识填充。路径缩写:old-src/ = onesystem/old-src/;analysis/ = onesystem/analysis/(由 old-src/oracle.dmp 解析出的 DDL)。

3.1 现有功能与调用链

层位置要点
Controllerold-src/contract/src/main/java/r1/projectmanage/controller/ContractSellQueryController.java:57/contract/contractSellQuery,类注释「合同台账查询」:51
Service.../service/impl/ContractSellQueryServiceImpl.java:53类注释「销售合同台账查询」:47
列表 SQL.../mapper/ContractSellQueryMapper.xml:7-206selectContractListPage;表 ESS_CONTRACT 见 :85
VO.../model/ContractSellVO.java:8@GaTableName("ESS_Contract")
权限工具.../util/UserPermissionUtils.java:30-39,88-97IsCompanyExist() / isDeptPermission()

3.2 列表主 SQL 的关键写法(实现时须逐条对照)

行为出处现行写法
数据范围硬过滤ContractSellQueryMapper.xml:124co.applyStatus = 'yqr'(只出「发展已确认」)
编号/名称模糊:126(name like ? or contractId like ? or economicsLawNumber like ?)——仅 3 列
分公司合同编号:168bccontractId like ?——独立参数,未并入上面的模糊条件
合同甲方:130;join :86-87cuo.name like ?(cuo = ess_customorganization)
日期区间:134-139只有「经法审批时间」区间 auditTimeStart/End,无 signedDate 区间
部门权限:144-146and (co.executionDepartment = ? or co.signedDepartment = ?)——双字段 OR
排序:205order by a.oid desc
去重:9,:11内外层 select distinct(一对多 join 会放大行)
派生列先例:40-48,:68收入金额 / 已开收据金额 / 匹配金额,全部是子查询聚合 + nvl(...,0)
分页ContractSellQueryServiceImpl.java:98;Controller:111-112框架 CountableRowBounds 按行分页

3.3 关键否定结论(三路一致)

销售侧不存在的东西——不要假设
  • ESS_CONTRACT 不含任何框架 / 订单 / 预估额 / 框架期限列(全表 136 列扫描,analysis/ddl/create_table.txt)。
  • 销售侧零命中「框架合同 / 订单合同」概念——命中项全部属于采购 / 外包域。
  • 易混淆项:QUALITYSTARTTIME / QUALITYENDTIME 是质保起止(对应导出表头「质保周期/月」),不是框架合同期限。
  • ESS_CONTRACT 已有 CONTRACTTYPE NVARCHAR2(1),但销售侧 VO 未映射它(VO 的 typeC 来自 TYPEC)——新增「合同类型2」不得复用该列。
  • 「导入日期」独立列不存在——老系统只有 DATATYPE='import' 标记 + CREATETIME。

3.4 术语撞车(必须在代码与词表里显式区分)

「框架合同」在采购 / 项目域是既有概念,与销售侧的「销售框架合同」同名不同义。全库含 FRAME 的列分布:

表列
R1_PURCHASECONTRACTFRAMENUMBER · FRAMEINDATESTART · FRAMEINDATEEND
R1_GOODSPLANDETAILISFRAME · FRAMENO
R1_PROJECTPLANDETAILISFRAME · FRAMENO
R1_PROJECTPROCESSRESULTFRAMENUMBER
ESS_CONTRACT(销售侧)无任何 FRAME 列——所以本功能要新增,且命名须避开 FRAME* 前缀带来的歧义
术语纪律 销售合同编号 = 需求文档里的历史称谓「经法号 / 经法编号」(2023-12 更名,ContractSellVO.java:20)。新系统一律用「销售合同编号」;仅在迁移映射与需求对照中保留旧名。老系统的列名(economicsLawNumber)、附件名(contract_f 经法合同审批单)与报错文案里旧词仍在,属历史包袱,不迁移。

3.5 导入链路(本功能的前置依赖)

事项出处事实
两套导入模板ContractSellImportServiceImpl.java:87-93,95-101逐项核对:均不含框架类型 / 框架预估金额 / 框架期限 / 各类预估额 / 订单→框架关联 / 分公司合同编号
落库状态:864,:1536;ProjectConstant.java:74导入写入的 applyStatus = wtj(未提交),而列表硬过滤 yqr → 导入数据默认查不到,须走确认流程
导入标记:860,:1532;ProjectConstant.java:926dataType = 'import'
判重:395-399,409-410按 economicsLawNumber / 名称判重,文案用「内部合同编号」
未写入的列ContractSellMapper.xml:214-275insertContractVo 不写 bccontractId、signCompany → 佐证「分公司合同编号」由另一篇(缺失的)导入需求承担
导出先例ContractSellQueryServiceImpl.java:68-79表头 54 列,含「分公司合同编号」「分公司合同归属单位」
取证方法提示(给后续维护者) old-src/oracle.dmp 是 1 GB 二进制文件,不可直读,仓库里也没有 oracle_dmp_表结构.sql 这个文件。权威表结构与索引只有一处来源:onesystem/analysis/ddl/create_table.txt 与 create_index.txt。注意 analysis/schema.json 只镜像了部分列(例:ESS_CONTRACT 的 DDL 有 136 列完整段 + 38 列在用子集两段),只看它会漏列。

04合并规范正文

本章是唯一实现依据。规则编号 FR-xx 用于需求追溯,R-xx 为边界规则,A-xx 为已定假设。每条都有唯一编号,评审意见请按编号提。

4.1 范围与目标

要做(Goals)

  • 在【合同台账查询(分公司)】下方新增只读的【框架合同信息查询】页面
  • 以「框架合同」为分组单位,成组展示其全部下属订单合同
  • 7 个筛选条件,其中 2 个具备穿透能力
  • 16 个展示字段,含 1 个查询期派生列
  • 2 个权限点 + 公司级 / 部门级数据范围
  • 支持搜索与导出,导出与列表同口径

不做(Non-Goals)

  • 不改造既有合同台账查询(/contract/contractSellQuery)的行为
  • 不新增第二张合同表——框架与订单靠单表自关联表达
  • 不实现通用行级数据权限引擎(切片期只留角色骨架)
  • 不承担框架/订单字段的写入路径(属姊妹需求,本功能只读)
  • 不处理采购侧框架合同——同名不同域,明确排除
  • 不做老系统改造,不存在代码级复用

4.2 数据模型

单表 sales_contract,镜像老系统 ESS_Contract 的在用字段(新系统字段名翻译为规范英文、语义不变),并按需求新增 8 列。不拆表:老系统销售合同(含分公司)本就是同表按字段区分,拆表会破坏迁移路径与编号规则。

框架合同(contractKind2 = framework) salesContractNo = LN2026-01-001 framePeriodStart / framePeriodEnd frameEstimatedAmount · estimateYear1 / 2 订单合同(contractKind2 = order) frameContractNo = LN2026-01-001 totalAmount · applyStatus 订单合同(contractKind2 = order) frameContractNo = LN2026-01-001 totalAmount · applyStatus 1 : N 自关联键:order.frameContractNo = framework.salesContractNo(= 销售合同编号,需求文档中的历史称谓「经法号」) ⚠ salesContractNo 在老库中无唯一索引(唯一索引仅 OID、CONTRACTID)→ 见边界规则 R-06
图 2|单表自关联。框架与订单不是两张表,而是同一张表里靠 contractKind2 区分、靠 frameContractNo 建立父子关系的两类记录。
表 4-1|字段清单:复用段(镜像老系统在用字段,语义不变)
新系统字段需求列名老系统列类型说明与出处
oid—OIDbigint PK主键,唯一索引 I100068
salesContractNo销售合同编号ECONOMICSLAWNUMBERtextNVARCHAR2(100)。ContractSellVO.java:20 注释「销售合同编号(2023-12月之前叫:经法编号)」——即需求中的「经法号」。框架—订单关联键
internalContractNo内部合同编号CONTRACTIDtextNVARCHAR2(50),唯一索引 UNIQUE_CONTRACT_CONTRACTID。ContractSellVO.java:14「2023-12月之前叫:合同编号」
branchContractNo分公司合同编号BCCONTRACTIDtextNVARCHAR2(50)。ContractSellVO.java:171「分公司合同编码」
contractName合同名称NAMEtext—
partyId / partyName合同甲方CONTRACTPARTYID → join ess_customorganization.namebigint / text甲方名称来自关联组织,见列表 SQL :86-87,130
signedDepartment签订部门SIGNEDDEPARTMENTbigint域 ESS_Contract_SignDept(ProjectConstant.java:311)。既是筛选下拉的数据对象,也是部门级权限的判定字段
executionDepartment—EXECUTIONDEPARTMENTbigint域 ESS_Contract_ExecutionDept(:313)。本功能不作为权限判定依据(见 §4.7)
signedDate签订日期SIGNEDDATEdate框架分组的排序键,可空
totalAmount合同金额/元TOTALAMOUNTdecimal(30,4)订单金额之和构成「已执行金额」
applyStatus—APPLYSTATUStext域 ESS_Contract_ApplyStatus;yqr=已确认(ProjectConstant.java:73)。可见性过滤依据
contractType—TYPECtext域 ESS_Contract_TypeC(:321)。保持不变,与新增的 contractKind2 并列
dataType—DATATYPEtextimport=导入(ProjectConstant.java:926)
createTime—CREATETIMEdatetime—
表 4-2|字段清单:新增段(需求要求,销售侧原本完全不存在)
新系统字段需求列名类型来源约束与说明
contractKind2合同类型2text新增枚举 framework=框架合同 / order=订单合同。不得复用 CONTRACTTYPE(老库已有 NVARCHAR2(1),占用情况未知)
frameContractNo其框架合同经法号text新增订单行存其所属框架的 salesContractNo;框架行此字段为空。需建索引
frameEstimatedAmount框架预估金额/元decimal(30,4)新增可空
framePeriodStart框架合同期限(起)date新增与下一列组成期限区间,是交集筛选的依据
framePeriodEnd框架合同期限(止)date新增若两者皆非空,须满足 start ≤ end(写入侧校验)
estimateYear1当年预估额/元decimal(30,4)新增可空
estimateYear2第二年预估额/元decimal(30,4)新增可空
importDate导入日期date新增语义=「该合同数据被导入系统的日期」,由导入动作写入。历史非导入数据留空(见 §2.2 裁决 4)
executedAmount已执行金额/元decimal派生不落库,查询期聚合。= 该框架下 applyStatus='yqr' 的订单 totalAmount 之和;无订单为 0。口径照抄老 SQL 同类子查询(ContractSellQueryMapper.xml:40-48,68)
写入侧校验(本功能不实现,但必须在需求上闭环) ① contractKind2 ∈ {framework, order};② 订单行的 frameContractNo 必填,且必须能匹配到一条框架合同的 salesContractNo;③ 框架行 frameContractNo 为空、framePeriodStart ≤ framePeriodEnd;④ 在「框架合同」范围内,salesContractNo 强制唯一,重复则报错、不静默入库。

4.3 筛选字段(7 个)

编号筛选条件交互规则穿透
FR-01合同编号/名称模糊输入命中 4 列任一即算中:销售合同编号、内部合同编号、分公司合同编号、合同名称(老系统仅 3 列,须补 BCCONTRACTID)是
FR-02合同类型2下拉选项:框架合同 / 订单合同。未选则不加过滤条件否
FR-03签订部门下拉覆盖 8 个事业部(含分公司)+ 营销中心。数据源=组织主数据,不复制老系统硬编码清单(ProjectConstant.java:78-96 的 SIGDEPT1–9)否
FR-04签订日期日期区间按 signedDate 过滤,端点计入;单侧为空按开区间处理,不报错否
FR-05合同甲方模糊输入按甲方名称(关联组织名)模糊匹配否
FR-06框架合同期限日期区间交集语义:查询区间与框架期限区间相交即命中,含端点;任一端为空视为该端无限(不因缺值漏筛)是
FR-07导入日期日期区间按 importDate 过滤;该列为空的记录被排除否
FR-08穿透规则:当使用 FR-01 或 FR-06 时,若命中的是订单合同,系统先上溯其框架合同,再带出该框架下全部订单合同,结果集 = 「框架合同 + 其下全部订单合同」—
FR-09除 FR-01/FR-06 外,其余筛选条件(FR-02–FR-05、FR-07)按实际匹配记录返回,不做整组展开—

4.4 展示字段(16 个)

复用 8 项 新增 5 项 派生 1 前端 1 待确认 1 16 个展示字段 = 8 复用 + 5 新增 + 1 派生(已执行金额)+ 1 前端生成(序号)+ 1 口径待确认(导入日期)
图 3|需求要 16 列,但工作性质完全不同:一半是现成字段直接搬;另一半要么新建列、要么查询期算、要么靠前端生成。
表 4-3|16 个展示字段逐列对照
#需求列名来源取值来源实现要点
1序号前端服务端计算(全局 seq)FR-11:仅框架行显示;跨页全局连续(第 2 页接着第 1 页计数),订单行留空
2合同类型2新增contractKind2FR-12:展示为中文「框架合同」/「订单合同」,不暴露枚举键
3销售合同编号复用salesContractNoFR-13:需求文档中的「经法号」即此列
4内部合同编号复用internalContractNoFR-14:唯一索引列
5分公司合同编号复用branchContractNoFR-15:同时是 FR-01 的模糊列之一
6合同名称复用contractNameFR-16
7合同甲方复用partyName(关联组织名)FR-17
8签订部门复用signedDepartment + 域翻译FR-18:须做 domain 翻译后展示部门名(参照 ContractSellQueryServiceImpl.java:156-160)
9签订日期复用signedDateFR-19:同时也是框架分组的排序键
10合同金额/元复用totalAmountFR-20:框架与订单两类行都展示自身金额
11框架预估金额/元新增frameEstimatedAmountFR-21:订单行此列为空是正常状态,不要显示 0
12已执行金额/元派生查询期聚合FR-22:仅框架行有值 = 其下 applyStatus='yqr' 的订单 totalAmount 之和;无已确认订单为 0;不受当前筛选影响(按该框架全量已确认订单汇总,见 R-04)
13当年预估额/元新增estimateYear1FR-23
14第二年预估额/元新增estimateYear2FR-24
15框架合同期限新增framePeriodStart ~ framePeriodEndFR-25:以日期时间段形式展示(起止),不是两个独立列
16导入日期待确认importDateFR-26:新增独立列(不复用创建时间)。历史数据留空 → 见确认项 C-03

金额格式统一:元、两位小数。新增列为空时展示为空,不显示 0 或占位符(否则会与真实 0 混淆)。

4.5 处理规则(4 条)与边界规则

编号规则内容
FR-27列表排序按框架合同的签订日期倒序;签订日期为空的框架排在最后;同一框架下的订单紧随其框架,不参与全局排序。须显式表达空值次序,不依赖数据库方言默认行为
FR-28框架—订单关系以订单行的「其框架合同经法号」(frameContractNo)作为父子关系依据,其值等于所属框架的销售合同编号
FR-29行样式框架合同行显示浅绿色;订单合同行正常显示
FR-30导出按当前筛选结果导出,字段与顺序与界面 16 列一致,导出为全量(不受分页与折叠限制)
FR-31搜索按当前全部筛选条件(含穿透类)重新查询并刷新列表
边界规则(三路未全部覆盖,本次合并后统一)
编号边界场景处理
R-01框架下无订单仍展示该框架行(不产生空行),已执行金额显示 0
R-02框架期限任一端为空该端视为无限,不因缺值漏筛;两端皆空时在任何期限区间下均命中(需在验收中确认业务是否接受)
R-03期限与查询区间仅端点相接判为命中(闭区间,含端点)
R-04已执行金额 vs 当前筛选按该框架下全部已确认订单汇总,不受当前筛选导致的部分订单未展示所影响(保证同一框架的金额稳定可复核)
R-05孤儿订单(frameContractNo 为空或指向不存在的框架)三层处置:① 分组列表中不展示(无父分组,强行展示会破坏分组语义与分页计数);② 产生可观测告警日志供运维发现;③ 导入侧前置校验拦截,使其不产生。→ 见确认项 C-02
R-06同一销售合同编号对应多条记录查询侧去重兜底(按 oid),不静默放大重复行;写入侧对「框架合同」范围内的编号强制唯一,重复导入直接报错
R-07单个框架下订单数极大组内可折叠(默认收起,按需展开)并设单组行数告警阈值;导出始终为全量,不受折叠影响
R-08筛选值为不存在的编号返回空结果集,不报错
R-09签订部门下拉出现不在主数据中的历史值筛选与展示均不丢失、不报错(原值透传,不静默丢弃)
R-10区间起止颠倒(start > end)自动交换后执行,不报错(参照老系统 ContractSellQueryServiceImpl.java:92-96 对金额区间的处理)

4.6 查询语义(实现难点集中在这里)

① 分组分页:分页单位是「框架合同」,不是「行」

这是本次合并中三路完全一致的关键决策,也是实现上最容易做错的一点:如果沿用老系统按行分页,会出现「框架行在第 1 页、它的订单行在第 2 页」的撕裂。

第 1 页(pageSize = 2 组) 1 F1 框架合同 · 已执行 100 O1 订单合同 · 100(已确认) O2 订单合同 · 200(未确认,不计入) 2 F2 框架合同 · 已执行 50 O3 订单合同 · 50(已确认) 页边界只在「组间」切分 第 2 页 3 F3 框架合同 · 已执行 0 无订单 → 仅框架行,不产生空行(R-01) 4 F4 框架合同 · 已执行 500 O4 订单合同 · 500(已确认)
图 4|序号 1–4 跨页连续;页边界只落在两个框架组之间。实现上是两段式:先对框架分页(LIMIT/OFFSET),再按本页框架的编号批量取订单,服务端组装成组。

实现要点(对应 FR-27 与 §2.1 第 2 项共识)

  • 两段式查询:先 SELECT ... FROM sales_contract WHERE contract_kind2='framework' [筛选] ORDER BY ... LIMIT ? OFFSET ?;再按本页框架的 salesContractNo 批量取订单。
  • 不能用单条分页 SQL 直接产出分组结果——老系统 CountableRowBounds 按行分页的做法必须改。
  • 空值排序显式化:ORDER BY signed_date IS NULL, signed_date DESC。不要依赖方言默认:SQLite 里 NULL 恰好排最后,但 PostgreSQL 语义不同,显式写死才安全。
  • totalRows 与 totalGroups 分开:前者只用于界面展示行数,后者才是分页计算依据。

② 穿透筛选:只有两个条件会「整组展开」

穿透类:合同编号/名称(FR-01)· 框架合同期限(FR-06) 筛选值:O1 的编号 命中订单 O1 上溯框架 F1 F1 + 其下全部订单(O1、O2)整组展示 其余筛选:类型2 · 签订部门 · 签订日期 · 合同甲方 · 导入日期 筛选值:某客户名 按实际匹配的记录返回,不展开为「框架 + 全部订单」 (FR-09)
图 5|穿透是一条「上溯 + 下钻」的路径。实现上是两段式:先解析出命中的框架编号集合(含向上补齐),再按下钻取整组。

③ 排序与派生列

项规则
组间排序框架合同按 signedDate 倒序;signedDate 为空的组排在最后(FR-27 / R-02)
组内排序订单行紧随其框架行,不单独按自身签订日期参与全局排序
已执行金额框架行 = 其下 applyStatus='yqr' 订单的 totalAmount 之和;无已确认订单为 0;订单行该列留空(FR-22 / R-04)
口径可比性金额聚合照抄老系统同类子查询风格(nvl(...,0) → COALESCE(...,0)),保证同一框架在两系统中的数字可比

4.7 权限与数据范围

编号权限点分配角色可见范围
FR-32框架合同信息查询-公司-查看
ESS_SALECONTRACT_FRAMECONTRACTQUERY_VIEW_COMPANY
公司销售合同管理员总、分公司全部框架合同及其订单,不做部门过滤
FR-33框架合同信息查询-部门-查看
ESS_SALECONTRACT_FRAMECONTRACTQUERY_VIEW_DEPT
事业部销售负责人(含分公司)仅 signedDepartment = 用户所属一级部门的框架合同及其下属订单
FR-34两者皆无拒绝访问,不返回任何数据(对应老系统 NULLPERMISSION 语义,ContractSellQueryController.java:107-110)
FR-35角色—权限绑定公司级 → 公司销售合同管理员;部门级 → 事业部销售负责人(含分公司)。角色定义在数据库中,仓库内未找到,须运维侧确认

判定顺序

  1. 命中公司级权限 → 不加任何范围条件(全量)
  2. 否则命中部门级权限 → 加条件 signedDepartment = 用户一级部门(取当前用户一级部门,参照 ContractSellQueryController.java:259-268)
  3. 两者皆无 → 抛无权限错误

组级权限(FR-36,见 §2.2 裁决 2)

权限判定落到框架行:一旦某框架行命中本部门,其下全部订单一并展示,组内不裁剪。穿透筛选命中的他部门框架,按同一规则裁剪——即不展示。
切片期只落上述骨架与权限码映射,不实现通用行级数据权限引擎。

4.8 查询契约(接口语义)

只定义语义,不绑定传输细节。入参 10 个筛选 + 2 个分页;出参为分组结构。

表 4-4|请求入参
参数类型必填语义对应
keywordstring否编号/名称模糊,命中 4 列任一;触发穿透FR-01
contractKind2enum否framework / orderFR-02
signedDepartmentstring(id)否签订部门(组织主数据 ID)FR-03
signedDateStart / signedDateEnddate否签订日期闭区间FR-04
partyNamestring否合同甲方模糊FR-05
framePeriodStart / framePeriodEnddate否框架期限查询区间;交集含端点,触发穿透FR-06
importDateStart / importDateEnddate否导入日期区间;空值记录被排除FR-07
pageint ≥ 1否页码,以框架合同组为单位§4.6
pageSizeint ≥ 1否每页组数§4.6
表 4-5|响应结构
字段语义
groups[]分组数组,每页 pageSize 组
groups[].seq序号,框架合同在结果集全局的顺序(跨页连续)
groups[].framework框架合同行(含 16 个展示字段)
groups[].orders[]该框架下全部订单合同行(可为空数组)
totalGroups框架组总数——分页计算依据
totalRows展示总行数(框架行 + 订单行)——仅用于展示,不参与分页
page / pageSize回显
表 4-6|错误与异常行为
场景行为
无任何查看权限返回无权限错误,不返回数据
入参类型/区间非法(start > end)校验层拦截;区间颠倒时按老系统做法自动交换(R-10)
无数据返回空 groups、totalGroups = 0,不报错(R-08)
存在孤儿订单不进入分组结果;写入可观测告警日志(R-05)

4.9 导出与搜索

导出(FR-30)

  • 入参 = 列表的同一套筛选参数(不含 page/pageSize)
  • 复用同一套筛选 / 权限 / 排序 / 分组逻辑——不允许另写一套查询
  • 输出列为界面 16 列,顺序一致
  • 全量导出,不受分页与组内折叠影响
  • 行区分(框架 / 订单)在导出中保留可读的分组表达

搜索(FR-31)

  • 按当前全部筛选条件(含穿透类)重新查询并刷新列表
  • 清空条件恢复默认视图(无筛选、第 1 页)
  • 搜索须重置到第 1 页,避免页码越界

4.10 前端要求

项要求
菜单入口位置在现有【合同台账查询(分公司)】下方(FR-10 的功能位置要求)
筛选区7 个控件 + 「搜索」「导出」按钮;签订部门为下拉(组织主数据)、签订日期/框架期限/导入日期为区间控件
分组渲染框架行(浅绿 + 序号)→ 其下订单行(常规、无序号、相对缩进),组与组之间有视觉间隔
序号列仅框架行有值;跨页连续
超大组默认折叠,按需展开(R-07)
无权限态明确的权限不足提示页,不显示空列表(避免被误读为「没有数据」)
列宽与右侧对齐金额列右对齐、千分位;日期列为空显示为空而非 null

05任务拆分

三路各给了任务拆分(A:8 张 tracer 票据;B:7 组 24 项;C:7 阶段 44 项)。本章合并为一套:沿用 A 的垂直切片结构(每片可独立演示、独立验收),吸收 C 的文件落点,吸收 B 的「每项自带验证方式」。

S1 字段落库与写入 S2 分组列表骨架 S3 常规筛选(6) S4 期限交集筛选 S6 展示字段与派生 S7 权限与数据范围 S5 穿透筛选 S8 导出(全量)
图 6|8 个垂直切片。S1 是硬前置(没有新字段就没有数据可查);S2 是 MVP(打通「数据 → 查询 → 页面」全链路);S3/S4 可并行;S8 汇聚三条支线。每个切片都能独立演示与验收。
编号任务落点验证方式
S1 · 前置:字段落库与写入路径 硬阻塞,无依赖,可立即开始
S1.1数据模型新增 8 列(见 §4.2 表 4-2),均允许为空server/sales-contract/schema.ts执行迁移后逐列存在且类型正确
S1.2枚举约束 contractKind2 ∈ {framework, order};contractKind2、frameContractNo 建索引schema.ts / constants.ts非法枚举写入被拒;按 frameContractNo 查询命中索引
S1.3导入模板扩列 + 落库写入新字段;importDate 于导入动作写入姊妹需求(导入模块)导入含框架信息的样例文件后,各列非空且 importDate = 导入当日
S1.4写入侧唯一性校验:框架范围内 salesContractNo 重复则报错,不静默入库姊妹需求(导入模块)重复编号导入被拒绝
S1.5写入侧父子完整性校验:订单 frameContractNo 必须匹配到框架姊妹需求(导入模块)悬空订单导入被拒绝(R-05 第 ③ 层)
S2 · 分组列表骨架(MVP) 依赖 S1
S2.1页面路由 + 菜单入口(【合同台账查询(分公司)】下方)app/(contract)/framework-contract-query/page.tsx菜单层级正确、点击可进入
S2.2框架分页查询(page/pageSize 以组为单位)server/sales-contract/queries.ts构造 2 组数据、pageSize=1,断言翻页后组完整
S2.3按本页框架编号批量取订单queries.ts1 个框架 + 3 订单 → 3 条订单随框架同页返回
S2.4组装分组:注入全局 seq、装配 orders、计算 totalGroups / totalRowsqueries.ts序号跨页连续;totalGroups ≠ totalRows 时以组数为分页依据
S2.5排序显式空值:ORDER BY signed_date IS NULL, signed_date DESCqueries.ts / rules.ts用例覆盖「倒序」「空值靠后」「订单不脱离框架」
S2.6分组渲染组件:框架行浅绿 + 序号;订单行常规、无序号、缩进components/FrameworkGroupList.tsx页面断言框架行背景浅绿、订单行序号列为空
S3 · 常规筛选(6 个非穿透) 依赖 S2
S3.1keyword 4 列模糊(补齐 branchContractNo)queries.ts以分公司合同编号片段查询能命中
S3.2contractKind2 下拉筛选queries.ts / FilterForm.tsx选「订单合同」结果只含订单;未选不加条件
S3.3签订部门下拉,数据源=组织主数据(不硬编码)FilterForm.tsx下拉项与需求「8 事业部 + 营销中心」逐项一致
S3.4签订日期区间筛选(端点计入,单侧为空按开区间)queries.ts区间内命中、边界当天命中
S3.5合同甲方模糊筛选queries.ts甲方名称片段命中
S3.6导入日期区间筛选(空值记录被排除)queries.ts历史无 importDate 的记录不出现
S3.7搜索按钮 / 条件清空(重置到第 1 页)FilterForm.tsx多条件搜索结果与直接调用查询一致
S4 · 框架期限交集筛选 依赖 S2
S4.1交集判定(含端点):框架起 ≤ 查询止 AND 框架止 ≥ 查询起rules.ts / queries.ts用例:完全包含 / 部分重叠 / 仅端点相接 / 完全不相交
S4.2空端处理:任一端为空视为该端无限rules.ts单端为空的框架在该条件下不漏筛(R-02)
S5 · 穿透筛选 依赖 S3 + S4
S5.1keyword 命中订单 → 上溯框架 → 带出该框架全部订单queries.ts按某订单编号查询,结果 = 其框架 + 该框架全部订单
S5.2期限交集命中 → 带出该框架全部订单queries.ts按区间查询,命中框架及其全部订单同组返回
S5.3非穿透条件不做整组展开queries.ts仅用「合同甲方」筛选,结果为实际匹配、不展开
S5.4同编号多记录时去重兜底(按 oid),不放大重复行queries.ts构造重复编号数据,断言组数与行数不翻倍(R-06)
S6 · 完整展示字段与派生列 依赖 S2
S6.116 列全部映射,含签订部门的 domain 翻译queries.ts / dto.ts样例记录逐列取值与来源一致
S6.2executedAmount 派生聚合(仅 yqr 订单;无订单为 0)queries.ts框架 3 订单(2 确认 1 未确认)→ 金额 = 2 条之和;无订单 → 0
S6.3新增列为空时展示为空(不显示 0 或占位符)dto.ts / 组件空值渲染为空字符串
S6.4订单行 executedAmount 留空组件订单行该格为空
S6.5金额格式统一(元、两位小数、右对齐、千分位)组件视觉核对
S7 · 权限与数据范围 依赖 S2
S7.1两个权限码常量constants.ts常量存在且被判定逻辑引用
S7.2判定:公司级全量 / 部门级 signedDepartment = 一级部门 / 皆无拒绝permissions.ts三类账号用例:全量 / 子集 / 拒绝
S7.3组级权限:框架命中则其全部订单可见(组内不裁剪)queries.ts框架属 A 部门、其订单属 B 部门 → 订单仍可见
S7.4穿透与权限叠加:命中他部门框架 → 按同一规则裁剪,不展示queries.ts部门级用户以他部门订单编号穿透,结果为空
S7.5角色骨架映射 + 无权限拒绝态页面permissions.ts / components/NoPermission.tsx权限不足显示明确提示,不显示空列表
S8 · 导出 依赖 S5 + S6 + S7
S8.1导出服务复用同一筛选 / 权限 / 排序 / 分组逻辑,全量输出server/sales-contract/export.ts导出内容与列表逐行一致
S8.2导出列 = 16 展示字段,且顺序一致export.ts列头比对
S8.3导出受同一数据范围约束export.ts部门级用户导出不含越权数据
跨切面(不阻塞主链路)
X1端到端验收 V1–V11(见第 6 章)tests/e2e/framework-query.spec.ts全部通过
X2与老系统口径比对:合同金额 / 已执行金额验证记录同等筛选下两系统数字一致
X3性能:常规数据量下首屏 < 3 秒验证记录实测
X4术语与 ADR 提案输出(CONTEXT.md / docs/adr 只读,以提案形式交付)提案文件可直接粘贴合入
X5孤儿订单可观测告警日志(配合 R-05)queries.ts存在孤儿数据时产生告警
并行建议 S3 与 S4 可并行(互不依赖);S6、S7 与 S3/S4 也可并行(都只依赖 S2)。queries.ts 是热点文件——多个任务改同一文件时串行提交,避免写冲突。

06验收标准

先备好这份最小数据集,再逐条走 V1–V11。这套数据专门覆盖了空值排序、未确认订单、无订单框架、跨部门四个边界。

表 6-1|验收种子数据(最小覆盖面)
编号类型签订部门签订日期审核状态框架期限说明
F1框架合同A 事业部2026-03-01已确认2026-01-01 ~ 2026-12-31下挂 O1、O2
F2框架合同A 事业部(空)已确认2025-06-01 ~ 2026-06-30下挂 O3;用于空值排序与交集筛选
F3框架合同B 事业部2026-02-01已确认2026-05-01 ~ 2027-04-30无订单;用于「无订单按 0」与部门权限
O1订单合同A 事业部2026-03-10已确认—frameContractNo = F1 编号;金额 100
O2订单合同A 事业部2026-04-01未确认—金额 200 —— 不应计入 F1 已执行金额
O3订单合同A 事业部2025-06-15已确认—frameContractNo = F2 编号;金额 50
表 6-2|验收场景(V1–V11)
#操作期望结果覆盖
V1公司级账号进入页面,不做筛选出现 F1 / F2 / F3 三个分组;F1 下 O1、O2,F2 下 O3,F3 无订单;框架行浅绿 + 序号,订单行无序号FR-10~12, FR-29
V2观察排序F1(2026-03-01)→ F3(2026-02-01)→ F2(空,排最后)FR-27, R-02
V3「合同编号/名称」输入 O1 的编号结果 = F1 框架 + F1 下全部订单(O1、O2)FR-08
V4「框架合同期限」选 2026-06-01 ~ 2026-06-10命中 F2(与 F2 期限有交集)并带出 O3FR-06, FR-08
V5仅用「合同甲方」筛选结果为实际匹配记录,不展开为框架 + 全部订单FR-09
V6查看 F1 的「已执行金额」= 100(仅计 O1;O2 未确认,不计入)FR-22, R-04
V7查看 F3 的「已执行金额」= 0(无订单)FR-22, R-01
V8部门级账号(A 事业部)查询只见 F1、F2 及其订单,看不到 F3(B 事业部)FR-33, FR-36
V9无权限账号访问被拒绝(无权限错误),不显示空列表FR-34
V10导出当前查询结果文件列 = 界面 16 列且顺序一致;行数 = 查询结果行数FR-30
V11「合同编号/名称」输入不存在的值空结果,不报错R-08
表 6-3|可度量成功标准
编号标准
SC-0116 个展示字段 100% 可见,且逐字段与需求文字对应
SC-02以任一订单编号做编号/名称筛选,结果 100% 包含其所属框架及该框架下全部订单(穿透正确率 100%)
SC-03以非穿透条件筛选时,穿透展开率 0
SC-04抽查 20 组,排序 100% 满足「按签订日期倒序 + 空值在末尾」,且框架行与其订单行同组、框架行在前
SC-05部门级用户结果中,签订部门非本部门的框架行数为 0(越权框架 0 条)
SC-06抽查若干框架,「已执行金额」100% 等于其下「已确认」订单金额之和(与手工汇总偏差 0)
SC-07导出列与界面 16 列一致,行数等于当前查询结果行数
SC-08常规数据量下,查询首屏 3 秒内呈现
额外硬要求(来自工程约束) 同等筛选条件下,本功能展示的「合同金额」与「已执行金额」必须与老系统台账口径可比(SC-09)。这既是数字正确性问题,也是迁移期的对账基础。

07待业务确认清单

这 4 项是不规范阻塞开发的口径类问题:开发按本文裁决先行,业务确认后如需调整,改动点都已定位到具体位置。建议由营销中心与分公司一并确认。本章可直接打印签字。

编号议题本文裁决(开发按此实现)若业务选择另一方案的代价业务确认
C-01 部门级权限按哪个字段判定 只认「签订部门」(照需求字面) 若需包含「执行部门」:权限条件加一个 OR,改动局部;但可见范围会变大
C-02 框架属本部门、订单属他部门时的可见性 组级权限:框架可见 → 其全部订单可见 若改为行级过滤:需重新定义分组呈现与分页语义(框架下订单会静默变少)
C-03 「导入日期」历史数据是否回填创建时间 不回填,历史数据留空;按导入日期筛选时被排除 若回填:手工录入的合同会被误判为「被导入过」,从第一天起污染该筛选口径
C-04 孤儿订单(指向的框架不存在)呈现方式 不展示 + 告警日志 + 导入侧拦截 若需界面可见:在列表末尾追加「未归属订单」伪分组,需同步调整分页与序号口径
另需业务/运维侧补充的信息(非口径问题,但同样阻塞联调)
编号缺失项为什么必须问
C-05《1.销售合同管理-分公司合同信息导入》需求文档本次输入未提供。决定新字段由谁写入、导入模板扩列清单,是 S1 切片的输入。三路一致标注为最大缺口
C-06角色「公司销售合同管理员」「事业部销售负责人」的定义角色配置在数据库,源码中未找到。需运维侧确认角色存在并完成权限绑定
C-07菜单【合同台账查询(分公司)】的实际落点该菜单名在仓库中未找到(菜单在数据库)。需确认新功能的挂点与既有功能的关系
C-08框架期限两端皆空时的筛选行为本文按「两端皆空 = 无限」处理(R-02),意味着它在任何期限区间下都会命中。需确认业务是否接受
C-09企业数据量级与单框架最大订单数决定 R-07 折叠阈值与分页大小,以及是否需要预聚合优化

08工程约束

以下 5 条是从 onesystem/docs/adr/0001–0004 与 CONTEXT.md 提炼的项目级原则。它们不是本功能的专属规定,但本功能必须遵守;与之冲突的设计须在评审中论证。

#原则对本功能的具体约束
I镜像老系统领域模型与编号规则
不可协商
数据模型原样镜像 ESS_Contract 在用字段,字段名翻译为规范英文但语义不变 → 直接决定了「单表自关联、不拆两张表」这条设计
II显式状态机,禁止工作流引擎本功能为纯查询、无审批流;只读引用既有合同审核状态枚举(applyStatus),不引入任何流程引擎
IIITypeScript 全栈 + ORM;切片 SQLite、生产 PostgreSQLNext.js + Drizzle + Zod;Vitest + Playwright。禁止书写 SQLite 特有方言的裸 SQL → 直接决定了空值排序必须显式写成 ORDER BY signed_date IS NULL, signed_date DESC,而不是依赖方言默认
IV证据可追溯 + 只读事实源一切技术断言须带 文件:行号;onesystem/ 只读,app/** 不得作为事实来源。本文档第 3 章即是该原则的产物
V简单优先(YAGNI),但合规/金额/权限边界必须显式不实现通用行级数据权限引擎、不做多表拆分、不做超出需求的抽象;但权限边界、金额口径(已执行金额)、审批相关规则必须显式建模并可测试

技术落点(建议,须与仓库现有布局对齐)

onesystem/app/
├── src/server/sales-contract/
│   ├── schema.ts         表定义(镜像 ESS_Contract 在用字段 + 8 个新增列)
│   ├── constants.ts      contractKind2 枚举、applyStatus、2 个权限码
│   ├── queries.ts        分组查询(框架分页 + 穿透 + 派生列)+ 导出查询
│   ├── rules.ts          期限交集、空值排序、区间交换
│   ├── permissions.ts    公司级 / 部门级判定
│   ├── export.ts         导出(列与展示字段对齐)
│   └── dto.ts            Zod:查询入参 / 行出参
└── src/app/(contract)/framework-contract-query/
    ├── page.tsx          查询页(筛选区 + 分组列表 + 导出)
    └── components/       FilterForm · FrameworkGroupList · ExportButton · NoPermission

tests/{unit,integration,e2e}/    Vitest 覆盖查询与规则;Playwright 覆盖 V1–V11

09附录

9.1 术语表(建议合入 CONTEXT.md 新增「合同侧」区)

词条释义禁用 / 易混(_Avoid_)
销售框架合同销售合同中的一类,代表在某一期限内对某客户的框架性约定,其下可挂多个订单合同。判定依据是「合同类型2 = 框架合同」框架合同(歧义:采购侧已有同名的「采购框架合同」,属不同域);大合同
销售订单合同挂在某个销售框架合同下的具体执行合同,通过「其框架合同经法号」指向所属框架子合同;订单(易与采购/投标语境混用)
销售合同编号框架↔订单自关联所用的业务编号;需求文档里历史称谓「经法号」在 2023-12 更名后的现行叫法。该编号在数据中无唯一性保证,作关联键时须辅以写入侧校验与查询侧去重经法号 / 经法编号(历史名,仅见于老数据与老文案);经济学法编号
其框架合同经法号需求文档中描述「订单合同指向其框架合同」的字段说法,其取值即所属框架的销售合同编号。新实现统一用「销售合同编号」表达框架编号(易与采购 FRAMENUMBER 混)
合同类型2合同在「框架—订单」两层结构中的角色枚举:框架合同 / 订单合同。与既有「合同类型(C)」是不同概念,不得混用或复用同一列合同类型(会与既有 C 型混淆)
框架合同期限框架合同的有效起止时间区间(起、止两个日期)质保周期(老系统 QUALITYSTARTTIME / QUALITYENDTIME 是质保起止,不是框架期限)

另有两条值得留档的架构决策(A 路提案,满足「难逆 / 非直觉 / 真取舍」):ADR 0005「销售框架与订单的关联键用销售合同编号,并加写入侧唯一性前置校验」;ADR 0006「框架合同查询以框架合同为分页单位」。理由是这两条看起来「本该用唯一列」「本该按行分页」,后来者容易改回去。

9.2 三路产物索引(可追溯合并来源)

来源产物
A · mattpocock/skillssdd-eval/A-mattpocock-skills/.scratch/sales-contract-framework-query/:research.md(证据链)· grilling.md(16 问决策台账)· spec.md(30 user story / 7 段模板)· context-proposal.md(术语与 ADR 提案)· issues/01–08-*.md(8 张 tracer 票据)· PROCESS.md
B · OpenSpecsdd-eval/B-openspec/openspec/changes/sales-contract-framework-query/:proposal.md · specs/sales-contract/framework-contract-query/spec.md(26 Requirement / 48 Scenario,validate --strict 通过)· design.md(9 个未定义点 + 决策 D-1~D-8)· tasks.md · PROCESS.md
C · spec-kitsdd-eval/C-spec-kit/:.specify/memory/constitution.md(5 条原则)· specs/001-sales-contract-framework-query/:spec.md(35 FR / 8 SC)· clarifications.md(16 个未定义点)· plan.md · research.md · data-model.md · contracts/query-contract.md · quickstart.md · tasks.md(44 任务)· analysis-report.md · PROCESS.md
评测总报告sdd-eval/README.md:三路覆盖矩阵、8 维度评分、隔离与红线核查记录

9.3 证据索引(关键结论 → 一手来源)

结论一手来源
台账查询入口与权限判定old-src/contract/src/main/java/r1/projectmanage/controller/ContractSellQueryController.java:51,57,92-117,259-268
列表 SQL(过滤 / 模糊 / 部门 / 排序 / 派生).../mapper/ContractSellQueryMapper.xml:7-206(:124 过滤、:126 模糊、:144-146 部门、:205 排序)
VO 字段与术语演进.../model/ContractSellVO.java:8,14,20,38,111,162,171
权限码常量.../model/ProjectConstant.java:73,311,313,321,494,496;ContractBackToArticleConrtoller.java:58
事业部硬编码 9 项.../model/ProjectConstant.java:78-96
导入模板与落库.../service/impl/ContractSellImportServiceImpl.java:64,87-101,864,1536;.../mapper/ContractSellMapper.xml:214-275
导出表头 54 列.../service/impl/ContractSellQueryServiceImpl.java:68-79
ESS_CONTRACT 完整 136 列与全库 FRAME 列分布onesystem/analysis/ddl/create_table.txt(由 old-src/oracle.dmp 解析)
唯一索引仅 OID、CONTRACTIDonesystem/analysis/ddl/create_index.txt
新系统架构决策onesystem/docs/adr/0001–0004;onesystem/CONTEXT.md

10评审者检查清单

评审会按此清单逐项过。任何一项不通过,请指出对应编号(FR-xx / R-xx / C-xx / Sx.x),便于直接定位改动。

开发 / 技术评审

  • 数据模型是否严格镜像老系统在用字段?8 个新增列的类型与约束是否认可?
  • 分组分页的两段式实现是否清晰?(FR-27、§4.6 ①)这是最容易做错的点
  • 空值排序的显式写法是否被接受?(ORDER BY signed_date IS NULL, signed_date DESC)
  • 穿透的两段式(上溯 + 下钻)是否有性能隐患?R-06 去重兜底是否够?
  • 已执行金额「不受当前筛选影响」(R-04)是否与产品预期一致?
  • R-05 孤儿订单的三层处置是否认可?告警日志的落点在哪?
  • 导出复用同一查询逻辑(FR-30)在现有代码结构下是否可行?
  • 任务切片的依赖图(图 6)与并行建议是否符合团队实际?

业务 / 产品评审

  • C-01 部门级权限字段:只认签订部门,是否与业务预期一致?
  • C-02 组级权限:部门级用户可能看到他部门的订单行,是否接受?
  • C-03 导入日期历史数据留空,导致按导入日期筛选查不到历史合同,是否接受?
  • C-04 孤儿订单不在界面展示,是否接受?
  • C-05 姊妹需求《分公司合同信息导入》文档何时能提供?(这是唯一硬阻塞)
  • C-08 框架期限两端皆空时在任何区间下都命中,是否符合业务口径?
  • 验收种子数据(表 6-1)与 V1–V11 是否覆盖了真实业务场景?
不建议在评审中重新打开的问题 以下 6 项已被三条独立路径一致确认(§2.1),重新讨论的成本高于收益:只显示已确认合同 · 以框架为分页单位 · 关联键用销售合同编号 + 双向兜底 · 期限交集含端点 · 合同类型2 独立新列 · 序号跨页全局连续。若确需变更,请说明新依据。