You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FreeMarker循环List检测下一项值 按ID分组输出XML及排错

FreeMarker模板实现采购单分组嵌套XML输出及宏调用排错方案

需求与初始实现问题

  • 底层数据源为数据库查询返回的采购明细平铺数据,需输出指定格式嵌套XML:
    • 同一采购单ID对应的所有明细归属到同一个<Purchasing>父节点
    • 父节点存储采购单维度字段:<ID>、<Client>、<Address>
    • 父节点下挂所有<PurchasingLine>明细子节点,子节点存储明细维度字段:<Line>、<Item>、<Qty>
  • 初始实现采用<#list Purchasing as po>直接遍历平铺明细,尝试通过判断下一条记录的ID、Line值控制<Purchasing>节点闭合,这种写法边界判断逻辑冗余,容易遗漏最后一组数据的闭合标签,维护成本高,不推荐使用。

可落地实现方案

方案1:使用FreeMarker内置?group_by函数实现(无需自定义宏,成本最低)

直接调用FreeMarker自带的分组、排序内置函数完成逻辑,不需要手动处理循环边界,代码样例如下:

<#-- 先按采购单ID、明细行号排序,再按采购单ID分组 -->
<#assign groupedPoList = Purchasing?sort_by(["ID", "Line"])?group_by("ID")>
<#list groupedPoList as poGroup>
<Purchasing>
  <#-- 采购单维度字段直接取当前分组第一条明细的对应值,同一采购单头字段一致 -->
  <ID>${poGroup.groupKey}</ID>
  <Client>${poGroup.items[0].Client}</Client>
  <Address>${poGroup.items[0].Address1} ${poGroup.items[0].Address2}</Address>
  <#-- 遍历当前分组下所有明细,生成子节点 -->
  <#list poGroup.items as line>
  <PurchasingLine>
    <Line>${line.Line}</Line>
    <Item>${line.Item}</Item>
    <Qty>${line.Qty}</Qty>
  </PurchasingLine>
  </#list>
</Purchasing>
</#list>

注意:如果同一采购单的头字段(Client/Address)在不同明细行存在空值,建议提前在SQL查询层做数据清洗,或在模板中通过?first取组内第一个非空值,避免输出空节点。

方案2:修正自定义listGroups宏调用方式(支持分组逻辑复用)

原有宏的核心逻辑无需修改,宏通过<#nested>向嵌套块传递两个参数:第一个是分组字段值(采购单ID),第二个是当前分组下的明细序列。修正后的调用代码如下:

<#-- 原有宏定义保持不变,无需新增参数 -->
<#macro listGroups items groupField>
  <#if items?size == 0><#return></#if>
  <#local sortedItems = items?sort_by(groupField)>
  <#local groupStart = 0>
  <#list sortedItems as item>
    <#if !item?is_first && item[groupField] != lastItem[groupField]>
      <#local groupEnd = item?index>
      <#nested lastItem[groupField], sortedItems[groupStart ..< groupEnd]>
      <#local groupStart = groupEnd>
    </#if>
    <#local lastItem = item>
  </#list>
  <#local groupEnd = sortedItems?size>
  <#nested lastItem[groupField], sortedItems[groupStart ..< groupEnd]>
</#macro>

<#-- 正确调用:分号后仅定义2个变量,按顺序接收宏传递的分组ID、分组明细序列 -->
<@listGroups Purchasing "ID"; poId, poLines>
<Purchasing>
  <ID>${poId}</ID>
  <Client>${poLines[0].Client}</Client>
  <Address>${poLines[0].Address1} ${poLines[0].Address2}</Address>
  <#list poLines as line>
  <PurchasingLine>
    <Line>${line.Line}</Line>
    <Item>${line.Item}</Item>
    <Qty>${line.Qty}</Qty>
  </PurchasingLine>
  </#list>
</Purchasing>
</@listGroups>

三类宏调用报错根因

  • 报错:嵌套块直接引用Client提示值为null或缺失
    根因:宏的嵌套块有独立作用域,不会自动继承外层循环的临时变量;且原有宏仅通过<#nested>传递2个参数,没有直接传递Client字段,直接引用时模板会在全局作用域查找该变量,找不到就触发空值报错。
  • 报错:追加"Client""Address1"等作为宏入参提示参数数量不匹配
    根因:宏定义时仅声明了items、groupField两个形参,调用时传入的额外参数属于多余实参,FreeMarker默认开启严格参数校验,参数数量不匹配时直接抛出错误。如果需要把字段作为宏入参传递,必须先修改宏定义的形参列表。
  • 报错:分号后追加Client等参数提示序列类型无法自动转换为字符串
    根因:宏调用时分号后的变量列表,是用来按顺序接收<#nested>传出的返回值的。原有宏仅传出2个值:第一个是字符串类型的分组ID,第二个是序列类型的分组明细列表。如果在分号后定义超过2个接收变量,多余变量会被赋值为null;如果变量顺序写错,把序列类型的明细列表赋值给了本该接收字符串的Client变量,输出时就会触发序列无法转字符串的类型错误。

内容的提问来源于stack exchange,提问作者mjieffect0909

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 11:27:13