打印模板草稿能保存,一点发布就报主从关系:我们把校验拆成了两档

创建采购单打印模板时,主表字段都能拖,子表明细却迟迟不出现在字段目录里------业务对象那边主从外键还在改。若这时直接拦截保存,设计器等于被外键配置绑架;若发布后也不拦,运行时又会静默少打明细行,财务对账才发现。

我们在 Forge Admin 打印链路里把这件事拆成了两档:设计草稿 softSkipIncompleteChildren,发布期 fail-loud 并给出可操作提示 。同一条链路还把模板 JSON 当不受信输入做协议硬校验,再用独立打印中心 + PrintDataProvider SPI 把「业务对象取数」和「已发布数据集取数」拆开------数据集路径声明不接受客户端 SQL / 表名 / 连接。

下面三段都带源码锚点(commit / 文件行),推断处标「按源码推演」。

现象:草稿能推进,发布突然报主从

典型现场有两种:

  1. 创建模板阶段 :子表 modelRefs.relations 为空,或外键列名对不上。设计目录仍能打开,主表字段可拖;残缺子表被跳过,不挡主表模板落地。
  2. 一点「发布打印模板」或走应用发布清单:同一套关系配置直接失败,错误文案指向「业务对象设计器」,并给出 ① relations ② masterDetailConfig ③ 外键列 三条检查。

对应修复提交:586d6a29178ba0d8cf84395644e8230b391e2f13(fix(print): 创建模板不因主从关系未配齐卡住,发布时给出可操作提示)。

根因不是「校验写晚了」,而是故意把「能否继续设计」和「能否上线出纸」分成两个契约。若设计期也严格,主从还在改时打印设计器无法开工;若发布期仍软跳过,上线后明细集合为空却无人知晓------这比报错更危险。

怎么改:一个布尔量贯穿解析

核心在 PrintMetadataResolver(约 472 行)。入口注释写得很直白:

java 复制代码
/**
 * @param softSkipIncompleteChildren true:设计草稿/创建模板允许跳过未配齐的子表关系;
 *                                   false:发布前必须配齐,否则给出可操作的拦截提示。
 */
public Metadata draft(..., boolean softSkipIncompleteChildren)

published(...) 固定传入 softSkipIncompleteChildren = false。关系条数不是「恰好一条」、外键列解析失败、或外键落在主键 / tenant_id 等系统列上时:

java 复制代码
if (relations.size() != 1) {
    // 创建/设计草稿:跳过该子表;发布必须严格,并给出可操作提示
    if (softSkipIncompleteChildren) {
        continue;
    }
    throw relationConfigInvalid(ref.getModelCode(), relations.size());
}

谁决定这个布尔量?低代码 Provider 在设计授权里按动作切换(LowcodePrintDataProvider):

java 复制代码
// 创建/打开/保存草稿:软跳过;发布打印模板:严格校验
boolean softSkip = action != PrintDesignAction.PUBLISH;
metadata.draft(actor, source, softSkip);

单测也钉死了反差:draftSkipsChildWhenRelationCountIsNotExactlyOne 允许跳过;draftStrictRejectsMissingRelationWithActionableMessage / publishedRejectsMissingRelationWithActionableMessage 要求文案里出现「业务对象设计器」。

两档策略对照表(可收藏)

维度 设计草稿(softSkip=true) 发布 / 已发布快照(softSkip=false)
调用入口 draft / candidate 默认 true;创建·打开·保存草稿 PUBLISH 动作;published(...);应用发布清单校验
残缺子表关系 continue 跳过该子表,主表可继续设计 relationConfigInvalid 拦截
外键列对不上 / 落在系统列 跳过子表 拦截,提示改主从配置
产品目标 不因外键未配齐卡住设计 fail-loud,防运行时静默丢明细
证据 PrintMetadataResolver 151--177 行;单测 draftSkips* 同文件 published;单测 *ActionableMessage

relationConfigInvalid 把「怎么修」写进异常,而不是只抛 path。缺关系时约等于:

  1. 子表 modelRefs.relations 是否声明了 sourceField / targetField
  2. masterDetailConfig 是否指定了该子表及外键
  3. 子表模型字段里是否真有该外键列

修好后:重新打开打印设计刷新字段目录,再保存或发布。这是给实施同学的检查清单,不是给开发看的堆栈。

发布失败自查清单

序号 你看到的现象 先查哪里 修完后
1 无法绑定子表「xxx」:未找到唯一外键 页面设计 → modelRefs.relations;主从配置 刷新打印字段目录再发布
2 匹配到 N 条主从关系(N≠1) 清理重复 relation,或在 masterDetailConfig.children 显式指定唯一一对字段 同上
3 关联字段无效(主键 / tenant_id 等) 主从配置改用子表指向主表的业务外键(如 xxxId) 同上
4 PRINT_VERSION_INVALID 发布模板完整性校验失败 模板 JSON 是否被改写、schemaHash 是否与规范化结果一致 按协议重新保存/发布版本
5 数据集打印参数报错 参数是否在数据集协议内;recordIdParam 是否被客户端覆盖 只传协议允许的业务参数

第二块:模板当不受信输入------PrintProtocolValidator

两档策略解决的是业务元数据是否配齐 ;模板本体是前端拖出来的 JSON,同样不能当内部可信对象。PrintProtocolValidator(commit 链路与 forge-plugin-print/.../protocol/)做的是协议层 hardening:

  • Jackson 开启 STRICT_DUPLICATE_DETECTION,重复键直接拒
  • StreamReadConstraints:嵌套深度 JSON_DEPTH = 64,字符串/文档上限对齐 DOCUMENT_BYTES = 1 MiB
  • 规则层再扫标识重复、节/元素数量等(PrintProtocolRules / PrintProtocolLimits)
  • canonicalize(键排序 + 数字规范化)后算 SHA-256 ,得到 schemaHash

类注释写明意图:验证与规范化是单次纯计算;不修改输入、不解析业务字段、不执行模板内容。 Parser 异常只打异常类型,不把原始片段回传前端------按源码推演,这是防探测 / 防日志泄漏的常见做法。

运行时还会再验一遍。PrintPrepareService 在列出可用模板与 prepare 出纸前:

java 复制代码
var document = protocol.validate(version.getSchemaJson());
if (!document.schemaHash().equals(version.getSchemaHash())) {
    throw PrintFailure.of(409, "PRINT_VERSION_INVALID", "发布模板完整性校验失败");
}

也就是说:库里存的版本不是「信任状」;每次准备打印都用同一套规范化规则重算哈希。草稿软跳过解决的是产品节奏,协议层硬校验解决的是「模板可被任意 JSON 污染」的安全边界。

第三块:独立打印中心 + SPI,数据集不准塞 SQL

低代码应用内打印之外,还有一批「系统页 / 自定义单据」也要打。提交 e056bf6cae7f1bb5c9dec50733601f303171580b 落地独立打印中心(前端 print/center/*、PRINT_CENTER.md):侧栏一个工作台完成「登记可打印业务 → 模板设计发布 → 挂载场景 → 启用」,业务页挂 BusinessPrintButton。

多数据源运行时在 451a439b0492b6efd6d5ca8234490ce936d56a76:PrintDataProvider SPI 要求业务模块明确实现授权 ,没有默认放行。PrintProviderRegistry 按 sourceType + supports 恰好选出一个 Provider,否则 PRINT_PROVIDER_UNAVAILABLE。

来源类型在配置校验里拆开(PrintSourceConfigValidator):

类型 配置约束 取数方式
SERVICE 必须选受管 providerCode,不能带 datasetId 业务模块实现的 Provider(如示例采购单)
DATASET 必须选已发布 datasetId,不能带 providerCode DatasetPrintDataProvider
API 直接拒绝「尚未开放」 ---

数据集适配器类注释是安全边界的一句话版:

java 复制代码
/**
 * 已发布数据集到打印协议的受控适配器。
 * 表名、SQL、连接和租户身份均不接受客户端输入。
 */
public class DatasetPrintDataProvider implements PrintDataProvider { ... }

prepareQuery 只使用来源上登记的 datasetId、服务端校验过的参数 Map、以及 mapping 里的 maxRows;recordIdParam 由服务端写入,调用方若试图覆盖会直接 PRINT_PARAMETER_INVALID 。PrintParameterValidator 另拦 tenantId / userId / templateVersionId 等保留键,参数体还有数量与 64KiB 上限。

按源码推演:这和「报表接口随便拼 SQL」是反着的------打印中心开放的是已发布数据集的受控查询入口,不是数据库控制台。

可带走的几条

  1. 设计期可软、发布期必须硬:软跳过是为了不堵设计,不是为了上线容忍脏关系;静默丢明细比报错更贵。
  2. 错误信息要可操作:把「去业务对象设计器看哪三处」写进异常,实施才用得上。
  3. 模板 JSON = 不受信输入:重复键、深度、大小、规范化哈希,发布入库与 prepare 出纸各验一次。
  4. 多数据源用 SPI 分型,而不是一个万能取数接口:SERVICE 走代码适配器,DATASET 只跑已发布数据集,客户端永远不带 SQL。

仓库:Forge Admin(Gitee)。若你也在做单据打印 / 低代码主从,欢迎点赞收藏,少踩「草稿卡住」和「上线少明细」这两类坑;有具体报错文案也可以评论区贴 path,我按协议字段帮你对一下该查 relations 还是 schemaHash。

相关推荐
CoderYanger1 小时前
一轮复习——F.动态规划模型总结(入门篇)
java·开发语言·数据结构·算法·leetcode·职场和发展·动态规划
小鹿的周先生1 小时前
第19章-Agent
java·spring·ai
溪语流沙1 小时前
【Web全栈进阶】FastAPI工程化:APIRouter拆分 + 配置 + 依赖注入
java·前端·fastapi
Bazingga1 小时前
Embabel学习笔记:把Agent的决策权从LLM手里拿回来
后端
用户8314550980311 小时前
基于 Firecracker 自建 Agent 沙箱集群:状态机 + 文件管理 + 进程编排,附 E2B 生产经验校准
后端
写后端的胖头鱼1 小时前
Redis 的 RDB 和 AOF
java·数据库·redis
cxoptics1 小时前
LBO 激光损伤阈值 LIDT 典型数值?如何避免晶体打坏?
java
逐米时代2 小时前
远程运维诊断减少到场率
java·服务器·前端
禾小西2 小时前
Redis AOF 日志:宕机了,如何避免数据丢失?
java·redis·mybatis