Skill: dongwangwangyangyan/xbridge-ext-helper (version 1.0.0)

--- name: xbridge-ext-helper description: | 帮助开发者在 xbridge3 框架中使用扩展字段（Extension Fields）。当用户需要： - 添加新的扩展字段到交易表 - 创建国家特定的扩展字段（如越南、马来西亚等） - 配置 @EntityColumnSchema 注解 - 使用 ExtensionUtils 读取/设置扩展字段 - 理解索引字段（idx_str/idx_date/idx_number）的分配 - 创建新的扩展Schema配置 时，使用此skill提供完整的代码示例和步骤指导。 关键词：xbridge, extension, ext_data, idx_str, idx_number, IndexedExtDataField, VirtualExtDataField, EntityColumnSchema --- # xbridge3 扩展字段助手 此技能帮助你在 xbridge3 框架中正确使用扩展字段机制。 ## 核心概念 ### 扩展字段类型 | 角色类型 | 存储位置 | 适用场景 | |---------|---------|---------| | `IndexedExtDataField` | JSON + 索引列 | 需要查询条件的字段，映射到 idx_str1-15 / idx_date1-5 / idx_number1-10 | | `VirtualExtDataField` | 仅 JSON | 不常查询的字段，纯扩展数据 | | `VirtualExtData` | 整个 extensions 对象 | 容器对象 | ### 索引字段配额 - **idx_str1 ~ idx_str15**: 15个字符串索引（VARCHAR 100） - **idx_date1 ~ idx_date5**: 5个日期索引（DATETIME） - **idx_number1 ~ idx_number10**: 10个数值索引（DECIMAL 35,13） ## 使用场景 ### 场景1：为新国家添加扩展字段 **步骤概览：** 1. 在 API 模块创建 CommonExtension 类（定义字段） 2. 在 Service 模块创建 ExtensionSchema 类（带 @EntityExtensionSchema 注解） 3. 实现 EntityTableSchemaConfigProvider 接口注册 Schema 4. 在 DAO 层处理扩展字段的保存和读取 **示例：创建 XX 国家扩展字段** ```java // ===== 步骤1: API 模块 - 定义扩展字段 ===== // document-engine-api/.../adaptor/xx/XXCommonExtension.java @Setter @Getter public class XXCommonExtension { // ExtensionObjectSpec 用于类型转换 public static final ExtensionObjectSpec<XXCommonExtension> KEY = ExtensionObjectSpec.<XXCommonExtension>builder() .name("xx") .type(XXCommonExtension.class) .defaultValueSupplier(XXCommonExtension::new) .build(); // 带索引的字段（需要查询） @EntityColumnSchema( title = "Invoice Type", columnName = "idx_str1", // 映射到索引字段 length = 6, columnRole = ColumnRoleType.IndexedExtDataField, listShowQuery = true ) private String invoiceType; @EntityColumnSchema( title = "Discount Amount", columnName = "idx_number1", length = 20, scale = 6, columnRole = ColumnRoleType.IndexedExtDataField ) private BigDecimal discountAmount; // 仅 JSON 存储的字段 @EntityColumnSchema( title = "Payment Method", columnRole = ColumnRoleType.VirtualExtDataField ) private String paymentMethodCode; // 复杂类型（JSON 数组） @EntityColumnSchema( title = "Item Codes", columnRole = ColumnRoleType.VirtualExtDataField, type = ColumnStandardType.EXT_JSON ) private List<String> itemCodes; } ``` ```java // ===== 步骤2: Service 模块 - 创建 Schema 类 ===== // document-engine-service/.../adaptor/xx/XXTransactionExtension.java @EntityExtensionSchema( tableId = "gbw_transaction", extNamespace = "xx" ) @Data public class XXTransactionExtension extends XXCommonExtension { } @EntityExtensionSchema( tableId = "gbw_transaction_line", extNamespace = "xx" ) @Data public class XXTransactionLineExtension extends XXCommonLineExtension { } ``` ```java // ===== 步骤3: 注册 Schema 配置 ===== // document-engine-service/.../adaptor/xx/XXTransactionProcessor.java @Component @RequiredArgsConstructor public class XXTransactionProcessor implements TransactionProcessor, EntityTableSchemaConfigProvider { @Override public EntityTableSchemaConfigItem getSchemaConfig() { EntityTableSchemaConfig xxSchema = EntitySchemaAnnotationParser.parseExtension( XXTransactionExtension.class, XXTransactionLineExtension.class ); return EntityTableSchemaConfigItem.builder() .tableSchemaID(EntityTableSchemaID.parse("global.XX")) .tableSchemaConfig(xxSchema) .build(); } // ... 其他处理器方法 } ``` ### 场景2：读取扩展字段值 ```java // 使用 ExtensionUtils 工具类 VNCommonExtension vnExt = ExtensionUtils.getExtension( "vn", // JSON 中的 key VNCommonExtension.class, // 目标类型 transaction.getExtensions() // extensions ObjectNode ); if (vnExt != null) { String invoiceType = vnExt.getInvoiceType(); BigDecimal discount = vnExt.getDiscountAmount(); } ``` ### 场景3：设置扩展字段值 ```java // 1. 获取或创建 ObjectNode ObjectNode extensions = transactionDTO.getExtensions(); if (extensions == null) { extensions = JacksonObjectMapper.get().createObjectNode(); } // 2. 创建扩展对象并设置值 XXCommonExtension xxExt = new XXCommonExtension(); xxExt.setInvoiceType("01/MTT"); xxExt.setDiscountAmount(new BigDecimal("100.00")); xxExt.setItemCodes(Arrays.asList("ITEM001", "ITEM002")); // 3. 转换为 JsonNode 并设置 String jsonString = JacksonObjectMapper.get().toJsonString(xxExt); JsonNode xxNode = JacksonObjectMapper.get().toJsonNode(jsonString); extensions.set("xx", xxNode); // 4. 设置回 DTO transactionDTO.setExtensions(extensions); ``` ### 场景4：在 DAO 中保存扩展字段 ```java @Repository @RequiredArgsConstructor public class TransactionDAO { private final TransactionMapper transactionMapper; private final TransactionExtMapper transactionExtMapper; public Transaction createTransaction(Transaction transaction) { // 1. 插入主表（框架自动处理 IndexedExtDataField 到索引字段的同步） EntityTableSchemaID tableSchemaID = transaction.getTableSchemaID(); EntityObjectManager<Transaction> entityManager = transactionMapper.getEntityObjectManager(tableSchemaID); entityManager.insert(transaction); // 2. 【重要】手动保存扩展表 TransactionExt transactionExt = new TransactionExt(); transactionExt.onCreateSetup(transaction); transactionExt.setExtData(transaction.getExtensions().toString()); transactionExtMapper.insert(transactionExt); return transaction; } public boolean updateTransaction(Transaction transaction) { // 1. 更新主表 EntityObjectManager<Transaction> entityManager = transactionMapper.getEntityObjectManager(transaction.getTableSchemaID()); entityManager.updateSelective(transaction, queryWrapper); // 2. 【重要】手动更新扩展表 TransactionExt transactionExt = new TransactionExt(); transactionExt.setTransactionId(transaction.getId()); transactionExt.setExtData(transaction.getExtensions().toString()); transactionExtMapper.updateById(transactionExt); return true; } } ``` ### 场景5：在 Service 中加载扩展字段 ```java public TransactionDTO queryTransactionDetail(Long transactionId) { // 1. 查询主表 Transaction transaction = transactionMapper.selectById(transactionId); // 2. 【重要】手动加载扩展字段 transaction.loadExtensions(); // 3. 加载明细行及扩展 transaction.loadLines(); for (TransactionLine line : transaction.getLines()) { line.loadExtensions(); } // 4. 转换为 DTO return transactionConverter.convertToDTO(transaction); } ``` ## @EntityColumnSchema 注解属性参考 | 属性 | 说明 | 常用值 | |-----|------|-------| | `title` | 字段标题（必填） | "Invoice Type" | | `type` | 字段标准类型 | `ColumnStandardType.STRING`, `DECIMAL`, `EXT_JSON` | | `columnRole` | **核心属性** - 列角色类型 | `IndexedExtDataField`, `VirtualExtDataField` | | `columnName` | 索引列名（IndexedExtDataField 用） | "idx_str1", "idx_number2" | | `length` | 最大长度（字符串）或精度（数值） | 100, 20 | | `scale` | 小数位数（数值类型） | 6, 4 | | `listShowQuery` | 列表查询时是否作为查询条件 | true / false | | `defaultValue` | 默认值 | "" | | `nullable` | 是否可空 | true / false | ## ColumnStandardType 类型 ```java AUTO // 自动推断 EXT_JSON // 扩展JSON字段（用于复杂类型如 List） ID_LONG // 长整型ID INSTANT_TIME // Instant时间类型 BOOLEAN // 布尔类型 DECIMAL // BigDecimal INTEGER // 整型 LONG_INT // 长整型 STRING // 字符串 ONE_TO_MANY // 一对多关联 ``` ## 索引字段分配建议 将最常用的查询条件字段映射到索引： 1. **idx_str1-5**: 发票类型、发票序列号、发票号码等高频查询字段 2. **idx_str6-10**: 业务类型编码、状态码等 3. **idx_str11-15**: 预留字段 4. **idx_number1-5**: 金额类字段（折扣金额、税额等） 5. **idx_number6-10**: 比例类字段（折扣率、税率等） ## 常见错误 ### 错误1：忘记手动保存扩展表 框架不会自动处理 `gbw_transaction_ext` 表的插入/更新，必须在 DAO 层手动处理。 ### 错误2：索引字段冲突 多个扩展字段映射到同一个 `columnName` 会导致数据覆盖。使用前应检查已有映射。 ### 错误3：忘记调用 loadExtensions() 查询后必须手动调用 `transaction.loadExtensions()` 才能获取扩展字段数据。 ### 错误4：Converter 层逐个字段转换扩展字段 扩展字段应该直接透传，不要逐个字段转换： ```java // 正确 target.setExtensions(source.getExtensions()); // 错误 - 不要这样做 VNCommonExtension vnExt = ExtensionUtils.getExtension("vn", ...); target.setInvoiceType(vnExt.getInvoiceType()); // ... 逐个字段转换 ``` ## 详细参考文档 如需了解更深入的实现细节，请阅读参考文件： ``` references/xbridge-ext-use.md - 交易表扩展字段使用指南（深入版） ``` 该文档包含： - 完整的数据库表结构设计说明 - xbridge3 框架扩展字段核心机制详解 - @EntityColumnSchema 注解所有属性详解 - 实体类完整配置示例 - 扩展字段增删改查完整流程 - 新国家/场景接入扩展字段的详细步骤 ## 项目参考文件 | 文件 | 路径 | |-----|------| | ExtensionUtils | `references/ExtensionUtils.java` | | VNCommonExtension | `references/VNCommonExtension.java` | | VNTransactionExtension | `references/VNTransactionExtension.java` | | VNTransactionProcessor | `references/VNTransactionProcessor.java` | | TransactionDAO | `references/TransactionDAO.java` | | TransactionConverter | `references/TransactionConverter.java` |

path: references/VNTransactionExtension.java
content: package com.baiwang.global.document.adaptor.vn;

import com.baiwang.global.transaction.adaptor.vn.VNCommonExtension;
import com.baiwang.xbridge3.database.extfields.annotation.EntityExtensionSchema;
import lombok.Data;


@EntityExtensionSchema(
    tableId = "gbw_transaction",
    extNamespace = "vn"
)
@Data
public class VNTransactionExtension extends VNCommonExtension {


}


path: references/TransactionConverter.java
content: package com.baiwang.global.document.converter;

import com.baiwang.global.document.dao.entity.Transaction;
import com.baiwang.global.document.dao.entity.TransactionLine;
import com.baiwang.global.transaction.consts.CountryCode;
import com.baiwang.global.transaction.consts.CurrencyCode;
import com.baiwang.global.transaction.consts.TransactionStatus;
import com.baiwang.global.transaction.consts.TransactionType;
import com.baiwang.global.transaction.model.dto.TransactionDTO;
import com.baiwang.global.transaction.model.dto.TransactionLineDTO;
import com.baiwang.global.transaction.utils.DateTimeUtils;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

@Component
public class TransactionConverter {

  /**
   * 将TransactionDTO转换为Transaction实体
   */
  public Transaction convertToEntity(TransactionDTO source) {
    Transaction target = new Transaction();
    target.setId(source.getId());
    target.setTenantId(source.getTenantId());
    target.setCompanyId(source.getCompanyId());
    // 基础字段映射
    target.setOriginalTransactionId(source.getOriginalTransactionId());
    target.setTransactionUuid(source.getTransactionUuid());
    target.setTransactionCode(source.getTransactionCode());
    target.setTransactionType(Optional.ofNullable(source.getTransactionType()).map(TransactionType::getCode).orElse(null));
    target.setTransactionDate(DateTimeUtils.toInstant(source.getTransactionDate()));
    target.setDocSourceCode(source.getDataSourceCode());
    target.setBusinessScenario(source.getBusinessScenario());
    if (source.getCountryCode() != null) {
      target.setCountryCode(source.getCountryCode().getCode());
    }
    target.setRegionCode(source.getRegionCode());
    target.setTaxLocationCode(source.getTaxLocationCode());
    target.setSellerName(source.getSellerName());
    target.setSellerAddress(source.getSellerAddress());
    target.setSellerPhoneNumber(source.getSellerPhoneNumber());
    target.setSellerBank(source.getSellerBank());
    target.setSellerBankAccount(source.getSellerBankAccount());
    target.setBuyerId(source.getBuyerId());
    target.setBuyerType(source.getBuyerType());
    target.setBuyerAddress(source.getBuyerAddress());
    target.setBuyerPhoneNumber(source.getBuyerPhoneNumber());
    target.setBuyerBank(source.getBuyerBank());
    target.setBuyerBankAccount(source.getBuyerBankAccount());
    target.setSalesOrderNo(source.getSalesOrderNo());
    target.setSalespersonCode(source.getSalespersonCode());
    target.setPurchaseOrderNo(source.getPurchaseOrderNo());
    target.setProcessCode(source.getProcessCode());
    target.setTransactionStatus(Optional.ofNullable(source.getTransactionStatus()).map(TransactionStatus::getStatus).orElse(null));
    target.setInvoiceStatus(source.getInvoiceStatus());
    target.setIssueMethod(source.getIssueMethod());
    target.setIssueDate(DateTimeUtils.toInstant(source.getIssueDate()));
    target.setTaxDate(source.getTaxDate());
    target.setErrorCode(source.getErrorCode());
    target.setErrorMsg(source.getErrorMsg());
    target.setTotalTaxCalculated(source.getTotalTaxCalculated());
    target.setTaxOverrideType(source.getTaxOverrideType());
    target.setTaxOverrideReason(source.getTaxOverrideReason());
    target.setAdjustReason(source.getAdjustReason());
    target.setVoidReason(source.getVoidReason());
    target.setReplaceReason(source.getReplaceReason());
    target.setBatchNo(source.getBatchNo());
    target.setDelFlag(source.getDelFlag());
    target.setDocRef1(source.getDocRef1());
    target.setDocRef2(source.getDocRef2());
    target.setDocRef3(source.getDocRef3());
    target.setCreateDate(DateTimeUtils.toInstant(source.getCreateDate()));
    target.setCreateBy(source.getCreateBy());
    target.setCreateById(source.getCreateById());
    target.setUpdateDate(DateTimeUtils.toInstant(source.getUpdateDate()));
    target.setUpdateBy(source.getUpdateBy());
    target.setUpdateById(source.getUpdateById());
    if (source.getCurrencyCode() != null) {
      target.setCurrencyCode(source.getCurrencyCode().getCode());
    }
    if (source.getExchangeRateCurrencyCode() != null) {
      target.setExchangeRateCurrencyCode(source.getExchangeRateCurrencyCode().getCode());
    }
    target.setExchangeRate(source.getExchangeRate());
    target.setExchangeRateEffectiveDate(source.getExchangeRateEffectiveDate());
    target.setCompanyCode(source.getCompanyCode());
    target.setSellerTaxId(source.getSellerTaxId());
    target.setSalesPointCode(source.getSalesPointCode());
    target.setBuyerCode(source.getBuyerCode());
    target.setBuyerName(source.getBuyerName());
    target.setBuyerTaxId(source.getBuyerTaxId());
    target.setBuyerEmail(source.getBuyerEmail());
    target.setNote(source.getNote());
    target.setTaxIncluded(source.getTaxIncluded());
    target.setTotalTaxAmount(source.getTotalTaxAmount());
    target.setTotalDiscountAmount(source.getTotalDiscountAmount());
    target.setTotalTaxExclusiveAmount(source.getTotalTaxExclusiveAmount());
    target.setTotalTaxInclusiveAmount(source.getTotalTaxInclusiveAmount());
    target.setTotalTaxInclusiveAmountIc(source.getTotalTaxInclusiveAmountIc());
    target.setTotalTaxExclusiveAmountIc(source.getTotalTaxExclusiveAmountIc());
    target.setTotalTaxAmountIc(source.getTotalTaxAmountIc());
    target.setTotalTaxExclusiveDiscountAmount(source.getTotalTaxExclusiveDiscountAmount());
    target.setTotalTaxExclusiveDiscountAmountIc(source.getTotalTaxExclusiveDiscountAmountIc());
    target.setTotalTaxInclusiveDiscountAmount(source.getTotalTaxInclusiveDiscountAmount());
    target.setTotalTaxInclusiveDiscountAmountIc(source.getTotalTaxInclusiveDiscountAmountIc());
    target.setTotalTaxCalculatedIc(source.getTotalTaxCalculatedIc());
    target.setExtensions(source.getExtensions());
    if (!ObjectUtils.isEmpty(source.getOriginalIssueDate())) {
      target.setOriginalIssueDate(DateTimeUtils.toInstant(source.getOriginalIssueDate()));
    }

    List<TransactionLine> lines = convertLinesToEntities(source.getLines());
    target.setLines(lines);

    // 去除所有 BigDecimal 字段的尾随零
    stripTransactionTrailingZeros(target);

    return target;
  }

  /**
   * 将TransactionLineDTO列表转换为TransactionLine实体列表
   */
  public List<TransactionLine> convertLinesToEntities(List<TransactionLineDTO> lineDTOs) {
    if (lineDTOs == null || lineDTOs.isEmpty()) {
      return new ArrayList<>();
    }

    List<TransactionLine> entities = new ArrayList<>();
    for (TransactionLineDTO dto : lineDTOs) {
      TransactionLine entity = convertLineToEntity(dto);
      entities.add(entity);
    }

    return entities;
  }

  /**
   * 将单个TransactionLineDTO转换为TransactionLine实体
   */
  private TransactionLine convertLineToEntity(TransactionLineDTO source) {
    TransactionLine target = new TransactionLine();

    target.setId(source.getId());
    target.setTransactionId(source.getTransactionId());
    target.setTenantId(source.getTenantId());
    target.setTransactionDate(DateTimeUtils.toInstant(source.getTransactionDate()));
    Integer lineType = source.getLineType();
    target.setLineType(Objects.isNull(lineType) ? 0 : lineType);
    target.setLineNumber(source.getLineNumber());
    target.setTaxIncluded(source.getTaxIncluded());
    target.setItemCode(source.getItemCode());
    target.setItemName(source.getItemName());
    target.setUnit(source.getUnit());
    target.setQuantity(source.getQuantity());
    target.setUnitPrice(source.getUnitPrice());
    target.setSpecification(source.getSpecification());
    target.setAmount(source.getAmount());
    target.setDiscountAmount(source.getDiscountAmount());
    target.setTaxRate(source.getTaxRate());
    target.setTaxAmount(source.getTaxAmount());
    target.setTaxCode(source.getTaxCode());
    target.setDescription(source.getDescription());
    target.setTaxExclusiveUnitPrice(source.getTaxExclusiveUnitPrice());
    target.setTaxExclusiveAmount(source.getTaxExclusiveAmount());
    target.setTaxInclusiveUnitPrice(source.getTaxInclusiveUnitPrice());
    target.setTaxInclusiveAmount(source.getTaxInclusiveAmount());
    target.setTaxCalculated(source.getTaxCalculated());
    target.setTaxSourcing(source.getTaxSourcing());
    target.setHsCode(source.getHsCode());
    target.setLineRef1(source.getLineRef1());
    target.setLineRef2(source.getLineRef2());
    target.setLineRef3(source.getLineRef3());
    target.setCreateDate(DateTimeUtils.toInstant(source.getCreateDate()));
    target.setCreateBy(source.getCreateBy());
    target.setCreateById(source.getCreateById());
    target.setUpdateDate(DateTimeUtils.toInstant(source.getUpdateDate()));
    target.setUpdateBy(source.getUpdateBy());
    target.setUpdateById(source.getUpdateById());
    target.setTaxAmountIc(source.getTaxAmountIc());
    target.setTaxExclusiveUnitPriceIc(source.getTaxExclusiveUnitPriceIc());
    target.setTaxInclusiveUnitPriceIc(source.getTaxInclusiveUnitPriceIc());
    target.setTaxExclusiveAmountIc(source.getTaxExclusiveAmountIc());
    target.setTaxInclusiveAmountIc(source.getTaxInclusiveAmountIc());
    target.setTaxExclusiveDiscountAmountIc(source.getTaxExclusiveDiscountAmountIc());
    target.setTaxInclusiveDiscountAmountIc(source.getTaxInclusiveDiscountAmountIc());
    target.setTaxCalculatedIc(source.getTaxCalculatedIc());
    target.setDiscountTaxCalculatedIc(source.getDiscountTaxCalculatedIc());
    target.setExtensions(source.getExtensions());

    return target;
  }

  /**
   * 将Transaction实体转换为TransactionDTO
   */
  public TransactionDTO convertToDTO(Transaction source) {
    TransactionDTO target = new TransactionDTO();
    target.setId(source.getId());
    target.setTenantId(source.getTenantId());
    target.setCompanyId(source.getCompanyId());
    // 基础字段映射
    target.setTransactionUuid(source.getTransactionUuid());
    target.setTransactionCode(source.getTransactionCode());
    target.setTransactionType(Optional.ofNullable(source.getTransactionType()).map(TransactionType::fromCode).orElse(null));
    target.setInvoiceStatus(source.getInvoiceStatus());
    target.setOriginalTransactionId(source.getOriginalTransactionId());
    target.setTransactionDate(DateTimeUtils.toOffsetDateTime(source.getTransactionDate()));
    target.setDataSourceCode(source.getDocSourceCode());
    target.setBusinessScenario(source.getBusinessScenario());
    target.setCountryCode(CountryCode.fromCode(source.getCountryCode()));
    target.setRegionCode(source.getRegionCode());
    target.setTaxLocationCode(source.getTaxLocationCode());
    target.setSellerName(source.getSellerName());
    target.setSellerAddress(source.getSellerAddress());
    target.setSellerPhoneNumber(source.getSellerPhoneNumber());
    target.setSellerBank(source.getSellerBank());
    target.setSellerBankAccount(source.getSellerBankAccount());
    target.setBuyerId(source.getBuyerId());
    target.setBuyerType(source.getBuyerType());
    target.setBuyerAddress(source.getBuyerAddress());
    target.setBuyerPhoneNumber(source.getBuyerPhoneNumber());
    target.setBuyerBank(source.getBuyerBank());
    target.setBuyerBankAccount(source.getBuyerBankAccount());
    target.setSalesOrderNo(source.getSalesOrderNo());
    target.setSalespersonCode(source.getSalespersonCode());
    target.setPurchaseOrderNo(source.getPurchaseOrderNo());
    target.setProcessCode(source.getProcessCode());
    target.setTransactionStatus(source.transactionStatusEnum());
    target.setIssueMethod(source.getIssueMethod());
    target.setIssueDate(DateTimeUtils.toOffsetDateTime(source.getIssueDate()));
    target.setTaxDate(source.getTaxDate());
    target.setErrorCode(source.getErrorCode());
    target.setErrorMsg(source.getErrorMsg());
    target.setTotalTaxCalculated(source.getTotalTaxCalculated());
    target.setTaxOverrideType(source.getTaxOverrideType());
    target.setTaxOverrideReason(source.getTaxOverrideReason());
    target.setAdjustReason(source.getAdjustReason());
    target.setVoidReason(source.getVoidReason());
    target.setReplaceReason(source.getReplaceReason());
    target.setBatchNo(source.getBatchNo());
    target.setDelFlag(source.getDelFlag());
    target.setDocRef1(source.getDocRef1());
    target.setDocRef2(source.getDocRef2());
    target.setDocRef3(source.getDocRef3());
    target.setCreateDate(DateTimeUtils.toOffsetDateTime(source.getCreateDate()));
    target.setCreateBy(source.getCreateBy());
    target.setCreateById(source.getCreateById());
    target.setUpdateDate(DateTimeUtils.toOffsetDateTime(source.getUpdateDate()));
    target.setUpdateBy(source.getUpdateBy());
    target.setUpdateById(source.getUpdateById());
    target.setCurrencyCode(CurrencyCode.fromCode(source.getCurrencyCode()));
    target.setExchangeRateCurrencyCode(CurrencyCode.fromCode(source.getExchangeRateCurrencyCode()));
    target.setExchangeRate(source.getExchangeRate());
    target.setExchangeRateEffectiveDate(source.getExchangeRateEffectiveDate());
    target.setCompanyCode(source.getCompanyCode());
    target.setSellerTaxId(source.getSellerTaxId());
    target.setSalesPointCode(source.getSalesPointCode());
    target.setBuyerCode(source.getBuyerCode());
    target.setBuyerName(source.getBuyerName());
    target.setBuyerTaxId(source.getBuyerTaxId());
    target.setBuyerEmail(source.getBuyerEmail());
    target.setNote(source.getNote());
    target.setTaxIncluded(source.getTaxIncluded());
    target.setTotalTaxAmount(source.getTotalTaxAmount());
    target.setTotalDiscountAmount(source.getTotalDiscountAmount());
    target.setTotalTaxExclusiveAmount(source.getTotalTaxExclusiveAmount());
    target.setTotalTaxInclusiveAmount(source.getTotalTaxInclusiveAmount());
    target.setTotalTaxInclusiveAmountIc(source.getTotalTaxInclusiveAmountIc());
    target.setTotalTaxExclusiveAmountIc(source.getTotalTaxExclusiveAmountIc());
    target.setTotalTaxAmountIc(source.getTotalTaxAmountIc());
    target.setTotalTaxExclusiveDiscountAmount(source.getTotalTaxExclusiveDiscountAmount());
    target.setTotalTaxExclusiveDiscountAmountIc(source.getTotalTaxExclusiveDiscountAmountIc());
    target.setTotalTaxInclusiveDiscountAmount(source.getTotalTaxInclusiveDiscountAmount());
    target.setTotalTaxInclusiveDiscountAmountIc(source.getTotalTaxInclusiveDiscountAmountIc());
    target.setTotalTaxCalculatedIc(source.getTotalTaxCalculatedIc());
    target.setExtensions(source.getExtensions());

    List<TransactionLineDTO> lineDTOS = convertLinesToDTOs(source.getLines());
    target.setLines(lineDTOS);

    return target;
  }

  /**
   * 将TransactionLine实体列表转换为TransactionLineDTO列表
   */
  public List<TransactionLineDTO> convertLinesToDTOs(List<TransactionLine> lineEntities) {
    if (lineEntities == null || lineEntities.isEmpty()) {
      return new ArrayList<>();
    }

    List<TransactionLineDTO> dtos = new ArrayList<>();
    for (TransactionLine entity : lineEntities) {
      TransactionLineDTO dto = convertLineToDTO(entity);
      dtos.add(dto);
    }

    return dtos;
  }

  /**
   * 将单个TransactionLine实体转换为TransactionLineDTO
   */
  private TransactionLineDTO convertLineToDTO(TransactionLine source) {
    TransactionLineDTO target = new TransactionLineDTO();

    target.setId(source.getId());
    target.setTransactionId(source.getTransactionId());
    target.setTenantId(source.getTenantId());
    target.setTransactionDate(DateTimeUtils.toOffsetDateTime(source.getTransactionDate()));
    target.setLineType(source.getLineType());
    target.setLineNumber(source.getLineNumber());
    target.setTaxIncluded(source.getTaxIncluded());
    target.setItemCode(source.getItemCode());
    target.setItemName(source.getItemName());
    target.setUnit(source.getUnit());
    target.setQuantity(source.getQuantity());
    target.setUnitPrice(source.getUnitPrice());
    target.setSpecification(source.getSpecification());
    target.setAmount(source.getAmount());
    target.setDiscountAmount(source.getDiscountAmount());
    target.setTaxRate(source.getTaxRate());
    target.setTaxAmount(source.getTaxAmount());
    target.setTaxCode(source.getTaxCode());
    target.setDescription(source.getDescription());
    target.setTaxExclusiveUnitPrice(source.getTaxExclusiveUnitPrice());
    target.setTaxExclusiveAmount(source.getTaxExclusiveAmount());
    target.setTaxInclusiveUnitPrice(source.getTaxInclusiveUnitPrice());
    target.setTaxInclusiveAmount(source.getTaxInclusiveAmount());
    target.setTaxCalculated(source.getTaxCalculated());
    target.setTaxSourcing(source.getTaxSourcing());
    target.setHsCode(source.getHsCode());
    target.setLineRef1(source.getLineRef1());
    target.setLineRef2(source.getLineRef2());
    target.setLineRef3(source.getLineRef3());
    target.setCreateDate(DateTimeUtils.toOffsetDateTime(source.getCreateDate()));
    target.setCreateBy(source.getCreateBy());
    target.setCreateById(source.getCreateById());
    target.setUpdateDate(DateTimeUtils.toOffsetDateTime(source.getUpdateDate()));
    target.setUpdateBy(source.getUpdateBy());
    target.setUpdateById(source.getUpdateById());
    target.setTaxAmountIc(source.getTaxAmountIc());
    target.setTaxExclusiveUnitPriceIc(source.getTaxExclusiveUnitPriceIc());
    target.setTaxInclusiveUnitPriceIc(source.getTaxInclusiveUnitPriceIc());
    target.setTaxExclusiveAmountIc(source.getTaxExclusiveAmountIc());
    target.setTaxInclusiveAmountIc(source.getTaxInclusiveAmountIc());
    target.setTaxExclusiveDiscountAmount(source.getTaxExclusiveDiscountAmount());
    target.setTaxInclusiveDiscountAmount(source.getTaxInclusiveDiscountAmount());
    target.setTaxExclusiveDiscountAmountIc(source.getTaxExclusiveDiscountAmountIc());
    target.setTaxInclusiveDiscountAmountIc(source.getTaxInclusiveDiscountAmountIc());
    target.setTaxCalculatedIc(source.getTaxCalculatedIc());
    target.setDiscountTaxCalculated(source.getDiscountTaxCalculated());
    target.setDiscountTaxCalculatedIc(source.getDiscountTaxCalculatedIc());
    target.setExtensions(source.getExtensions());

    return target;
  }

  /**
   * 去除 Transaction 实体中所有 BigDecimal 字段的尾随零
   */
  private void stripTransactionTrailingZeros(Transaction transaction) {
    if (transaction == null) {
      return;
    }

    transaction.setTotalTaxAmount(stripDecimal(transaction.getTotalTaxAmount()));
    transaction.setTotalTaxExclusiveAmount(stripDecimal(transaction.getTotalTaxExclusiveAmount()));
    transaction.setTotalTaxInclusiveAmount(stripDecimal(transaction.getTotalTaxInclusiveAmount()));
    transaction.setTotalDiscountAmount(stripDecimal(transaction.getTotalDiscountAmount()));
    transaction.setTotalTaxCalculated(stripDecimal(transaction.getTotalTaxCalculated()));
    transaction.setTotalTaxAmountIc(stripDecimal(transaction.getTotalTaxAmountIc()));
    transaction.setTotalTaxExclusiveAmountIc(stripDecimal(transaction.getTotalTaxExclusiveAmountIc()));
    transaction.setTotalTaxInclusiveAmountIc(stripDecimal(transaction.getTotalTaxInclusiveAmountIc()));
    transaction.setTotalTaxExclusiveDiscountAmountIc(stripDecimal(transaction.getTotalTaxExclusiveDiscountAmountIc()));
    transaction.setTotalTaxInclusiveDiscountAmountIc(stripDecimal(transaction.getTotalTaxInclusiveDiscountAmountIc()));
    transaction.setTotalTaxCalculatedIc(stripDecimal(transaction.getTotalTaxCalculatedIc()));
    transaction.setTotalTaxExclusiveDiscountAmount(stripDecimal(transaction.getTotalTaxExclusiveDiscountAmount()));
    transaction.setTotalTaxInclusiveDiscountAmount(stripDecimal(transaction.getTotalTaxInclusiveDiscountAmount()));
    transaction.setExchangeRate(stripDecimal(transaction.getExchangeRate()));

    if (CollectionUtils.isNotEmpty(transaction.getLines())) {
      transaction.getLines().forEach(this::stripTransactionLineTrailingZeros);
    }
  }

  /**
   * 去除 TransactionLine 中所有 BigDecimal 字段的尾随零
   */
  private void stripTransactionLineTrailingZeros(TransactionLine line) {
    if (line == null) {
      return;
    }

    line.setQuantity(stripDecimal(line.getQuantity()));
    line.setUnitPrice(stripDecimal(line.getUnitPrice()));
    line.setAmount(stripDecimal(line.getAmount()));
    line.setDiscountAmount(stripDecimal(line.getDiscountAmount()));
    line.setTaxRate(stripDecimal(line.getTaxRate()));
    line.setTaxAmount(stripDecimal(line.getTaxAmount()));
    line.setTaxAmountIc(stripDecimal(line.getTaxAmountIc()));
    line.setTaxExclusiveUnitPrice(stripDecimal(line.getTaxExclusiveUnitPrice()));
    line.setTaxInclusiveUnitPrice(stripDecimal(line.getTaxInclusiveUnitPrice()));
    line.setTaxExclusiveUnitPriceIc(stripDecimal(line.getTaxExclusiveUnitPriceIc()));
    line.setTaxInclusiveUnitPriceIc(stripDecimal(line.getTaxInclusiveUnitPriceIc()));
    line.setTaxExclusiveAmount(stripDecimal(line.getTaxExclusiveAmount()));
    line.setTaxInclusiveAmount(stripDecimal(line.getTaxInclusiveAmount()));
    line.setTaxExclusiveAmountIc(stripDecimal(line.getTaxExclusiveAmountIc()));
    line.setTaxInclusiveAmountIc(stripDecimal(line.getTaxInclusiveAmountIc()));
    line.setTaxExclusiveDiscountAmount(stripDecimal(line.getTaxExclusiveDiscountAmount()));
    line.setTaxInclusiveDiscountAmount(stripDecimal(line.getTaxInclusiveDiscountAmount()));
    line.setTaxExclusiveDiscountAmountIc(stripDecimal(line.getTaxExclusiveDiscountAmountIc()));
    line.setTaxInclusiveDiscountAmountIc(stripDecimal(line.getTaxInclusiveDiscountAmountIc()));
    line.setTaxCalculated(stripDecimal(line.getTaxCalculated()));
    line.setDiscountTaxCalculated(stripDecimal(line.getDiscountTaxCalculated()));
    line.setTaxCalculatedIc(stripDecimal(line.getTaxCalculatedIc()));
    line.setDiscountTaxCalculatedIc(stripDecimal(line.getDiscountTaxCalculatedIc()));
  }

  /**
   * 安全地去除 BigDecimal 的尾随零
   */
  private BigDecimal stripDecimal(BigDecimal value) {
    if (value == null) {
      return null;
    }

    // 直接去除尾随零
    return value.stripTrailingZeros();
  }
}

path: references/TransactionDAO.java
content: package com.baiwang.global.document.dao;

import com.baiwang.global.document.dao.entity.Transaction;
import com.baiwang.global.document.dao.entity.TransactionExt;
import com.baiwang.global.document.dao.entity.TransactionLine;
import com.baiwang.global.document.dao.entity.TransactionLineExt;
import com.baiwang.global.document.dao.mapper.*;
import com.baiwang.global.transaction.consts.TransactionStatus;
import com.baiwang.global.transaction.consts.TransactionType;
import com.baiwang.xbridge3.common.json.JacksonObjectMapper;
import com.baiwang.xbridge3.database.core.query.EntityQueryWrapper;
import com.baiwang.xbridge3.database.extfields.model.EntityTableSchemaID;
import com.baiwang.xbridge3.database.mybatisplus.EntityObjectManager;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 聚合 mapper 操作
 */
@Repository
@RequiredArgsConstructor
public class TransactionDAO {

  private final TransactionMapper transactionMapper;
  private final TransactionLineMapper transactionLineMapper;
  private final TransactionExtMapper transactionExtMapper;
  private final TransactionLineExtMapper transactionLineExtMapper;
  private final TransactionLineAddressMapper transactionLineAddressMapper;

  /**
   * 保存交易主记录
   */
  public Transaction createTransaction(Transaction transaction) {
    EntityTableSchemaID tableSchemaID = transaction.getTableSchemaID();
    EntityObjectManager<Transaction> transactionEntityManager = transactionMapper.getEntityObjectManager(tableSchemaID);
    EntityObjectManager<TransactionLine> transactionLineEntityManager = transactionLineMapper.getEntityObjectManager(tableSchemaID);

    // 维护交易记录版本号
    if (transaction.getOriginalTransactionId() == null) {
      transaction.setVersionNo(0);
    } else {
      if (transaction.transactionTypeEnum() == TransactionType.AdjustInvoice) {
        // 调整发票取最大的 + 1
        Integer maxVersionNo = transactionMapper.selectMaxVersionNoByOriginalTransactionId(transaction.getOriginalTransactionId());
        Assert.notNull(maxVersionNo, "Original transaction does not exists!");
        transaction.setVersionNo(maxVersionNo + 1);
      } else {
        // 其他取原发票版本 + 1
        Transaction originalTransaction = transactionMapper.selectById(transaction.getOriginalTransactionId());
        Assert.notNull(originalTransaction, "Original transaction does not exists!");
        transaction.setVersionNo(originalTransaction.getVersionNo() + 1);
      }
    }
    // 设置默认值
    transaction.setDelFlag(0); // 未删除
    transaction.setCreateDate(Instant.now());
    transaction.setUpdateDate(Instant.now());
    // 新增
    transactionEntityManager.insert(transaction);
    if (!CollectionUtils.isEmpty(transaction.getLines())) {
      transaction.getLines().forEach(item -> item.onCreateSetup(transaction));
      transactionLineEntityManager.insertMultiple(transaction.getLines());
      for (TransactionLine line : transaction.getLines()) {
        insertTransactionLineExt(line);
      }
    }
    // 扩展字段存储
    TransactionExt transactionExt = new TransactionExt();
    transactionExt.onCreateSetup(transaction);
    transactionExt.setExtData(transaction.getExtensions().toString());
    transactionExtMapper.insert(transactionExt);
    return transaction;
  }

  public boolean updateTransaction(Transaction transaction, Collection<TransactionStatus> expectedStatus) {
    EntityTableSchemaID tableSchemaID = transaction.getTableSchemaID();
    EntityObjectManager<Transaction> transactionEntityManager = transactionMapper.getEntityObjectManager(tableSchemaID);
    EntityObjectManager<TransactionLine> transactionLineEntityManager = transactionLineMapper.getEntityObjectManager(tableSchemaID);
    transaction.setUpdateDate(Instant.now());
    EntityQueryWrapper queryWrapper = EntityQueryWrapper.columnNameQuery(transactionEntityManager.getEntityTable());
    queryWrapper.eq("id", transaction.getId());
    if (!expectedStatus.isEmpty()) {
      queryWrapper.in("transaction_status", expectedStatus.stream().map(TransactionStatus::getStatus).toList());
    }
    // 更新
    int updated = transactionEntityManager.updateSelective(
        transaction, queryWrapper
    );
    if (updated <= 0) {
      return false;
    }

    TransactionExt transactionExt = new TransactionExt();
    transactionExt.setTransactionId(transaction.getId());
    transactionExt.setExtData(transaction.getExtensions().toString());
    transactionExtMapper.updateById(transactionExt);

    List<TransactionLine> existsLines = transactionLineMapper.selectList(
        Wrappers.lambdaQuery(TransactionLine.class)
            .eq(TransactionLine::getTransactionId, transaction.getId())
    );
    Map<Long, TransactionLine> toRemoveLinesMap = existsLines.stream()
        .collect(Collectors.toMap(TransactionLine::getId, Function.identity()));
    if (!CollectionUtils.isEmpty(transaction.getLines())) {
      for (TransactionLine line : transaction.getLines()) {
        TransactionLine existsLine = toRemoveLinesMap.remove(line.getId());
        if (existsLine != null) {
          line.onUpdateSetup(transaction);
          // 更新明细行
          transactionLineEntityManager.updateByIdSelective(line);
          TransactionLineExt lineExt = new TransactionLineExt();
          lineExt.setLineId(line.getId());
          lineExt.setExtData(line.getExtensions().toString());
          transactionLineExtMapper.updateById(lineExt);
        } else {
          line.onCreateSetup(transaction);
          if (line.getCreateDate() == null) {
            line.setCreateDate(Instant.now());
          }
          // 新增明细行
          transactionLineEntityManager.insertSelective(line);
          insertTransactionLineExt(line);
        }
      }
      List<Long> toRemoveLineIdList = toRemoveLinesMap.values().stream().map(TransactionLine::getId).toList();
      transactionLineMapper.deleteByIds(toRemoveLineIdList);
      transactionLineExtMapper.deleteByIds(toRemoveLineIdList);
    }
    return true;
  }

  private void insertTransactionLineExt(TransactionLine line) {
    TransactionLineExt lineExt = new TransactionLineExt();
    lineExt.onCreateSetup(line);
    ObjectNode extensions = line.getExtensions();
    if (extensions == null) {
      extensions = JacksonObjectMapper.get().createObjectNode();
    }
    lineExt.setExtData(extensions.toString());
    transactionLineExtMapper.insert(lineExt);
  }


  /**
   * 根据交易UUID查找交易记录
   */
  public Transaction findByTransactionUuid(Long tenantId, String transactionUuid) {
    return transactionMapper.selectOne(
        Wrappers.lambdaQuery(Transaction.class)
            .eq(Transaction::getTenantId, tenantId)
            .eq(Transaction::getTransactionUuid, transactionUuid)
    );
  }


  /**
   * 根据交易UUID查找交易记录
   */
  public Transaction findByTransactionId(Long transactionId) {
    return transactionMapper.selectById(transactionId);
  }
}

path: references/VNTransactionProcessor.java
content: package com.baiwang.global.document.adaptor.vn;

import com.alibaba.fastjson.JSON;
import com.baiwang.global.document.calculator.TransactionTaxCalculator;
import com.baiwang.global.document.calculator.TransactionTaxContext;
import com.baiwang.global.document.dao.entity.Transaction;
import com.baiwang.global.document.dao.entity.TransactionLine;
import com.baiwang.global.document.service.plugin.TransactionExtensionManager;
import com.baiwang.global.document.service.plugin.TransactionValidator;
import com.baiwang.global.document.service.plugin.model.*;
import com.baiwang.global.document.service.vn.discount.DiscountCalculationService;
import com.baiwang.global.document.service.vn.discount.DiscountDistributionContext;
import com.baiwang.global.document.service.TransactionHistoryService;
import com.baiwang.global.document.service.plugin.TransactionProcessor;
import com.baiwang.global.transaction.consts.CountryCode;
import com.baiwang.global.transaction.consts.CurrencyCode;
import com.baiwang.global.transaction.consts.TransactionType;
import com.baiwang.xbridge3.database.extfields.model.EntityTableSchemaConfig;
import com.baiwang.xbridge3.database.extfields.model.EntityTableSchemaID;
import com.baiwang.xbridge3.database.extfields.spi.EntityTableSchemaConfigItem;
import com.baiwang.xbridge3.database.extfields.spi.EntityTableSchemaConfigProvider;
import com.baiwang.xbridge3.database.extfields.support.EntitySchemaAnnotationParser;
import com.google.common.collect.Sets;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Set;
import java.util.function.Function;

@Slf4j
@Component
@RequiredArgsConstructor
public class VNTransactionProcessor implements TransactionProcessor, EntityTableSchemaConfigProvider {

  static final Set<TransactionBusinessRoute> ROUTES = Sets.newHashSet(
      TransactionBusinessRoute.builder()
          .countryCode(CountryCode.VN)
          .transactionType(TransactionType.SalesInvoice)
          .build(),
      TransactionBusinessRoute.builder()
          .countryCode(CountryCode.VN)
          .transactionType(TransactionType.AdjustInvoice)
          .build(),
      TransactionBusinessRoute.builder()
          .countryCode(CountryCode.VN)
          .transactionType(TransactionType.ReplaceInvoice)
          .build()
  );

  private final TransactionTaxCalculator taxCalculator;
  private final TransactionHistoryService transactionHistoryService;
  private final DiscountCalculationService discountCalculationService;
  private final TransactionExtensionManager transactionExtensionManager;

  @Override
  public boolean supports(TransactionBusinessRoute route) {
    return ROUTES.contains(route);
  }

  @Override
  public EntityTableSchemaConfigItem getSchemaConfig() {
    EntityTableSchemaConfig vnTransactionSchema = EntitySchemaAnnotationParser.parseExtension(
        VNTransactionExtension.class,
        VNTransactionLineExtension.class
    );
    return EntityTableSchemaConfigItem.builder()
        .tableSchemaID(EntityTableSchemaID.parse("global.VN"))
        .tableSchemaConfig(vnTransactionSchema)
        .build();
  }

  @Override
  public void beforeCreateTransaction(TransactionCreateContext context) {
    Transaction transaction = context.getTransaction();

    // 1. 【关键步骤】保存原始单据到历史表（在折扣计算前执行）
    //saveOriginalTransactionToHistory(transaction, "CREATE");

    // 2. 处理整单折扣计算
    processCommercialDiscount(context);

    // 3. 执行金额计算和价税分离
    doCalculateAmount(transaction);
  }

  private void doCalculateAmount(Transaction transaction) {
    TransactionTaxContext taxContext = TransactionTaxContext.builder()
        .amountRounding(amountRounding(transaction))
        .invoiceAmountRounding(invoiceAmountRounding(transaction))
        .unitPriceRounding(unitPriceRounding(transaction))
        .quantityRounding(quantityRounding(transaction))
        .transaction(transaction)
        .build();
    // 金额精度预处理
    for (TransactionLine line : transaction.getLines()) {
      if (line.getQuantity() != null) {
        line.setQuantity(taxContext.getQuantityRounding().apply(line.getQuantity()));
      }
      if (line.getUnitPrice() != null) {
        line.setUnitPrice(taxContext.getUnitPriceRounding().apply(line.getUnitPrice()));
      }
      if(line.getAmount() != null){
        line.setAmount(taxContext.getAmountRounding().apply(line.getAmount()));
      }
      if (line.getDiscountAmount() != null) {
        line.setDiscountAmount(taxContext.getAmountRounding().apply(line.getDiscountAmount()));
      }
      if (line.getTaxAmount() != null) {
        line.setTaxAmount(taxContext.getAmountRounding().apply(line.getTaxAmount()));
      }
    }
    // 价税分离计算
    taxCalculator.calculateVATTax(taxContext);
  }

  @Override
  public void beforeUpdateTransaction(TransactionUpdateContext context) {
    Transaction transaction = context.getTransaction();

/*    // 1. 【关键步骤】保存原始单据到历史表（在折扣计算前执行）
    String operationType = transaction.getTransactionType();
    if (TransactionType.ReplaceInvoice.getCode().equals(operationType)) {
      saveOriginalTransactionToHistory(transaction, "REPLACE");
    } else if (TransactionType.AdjustInvoice.getCode().equals(operationType)) {
      saveOriginalTransactionToHistory(transaction, "ADJUST");
    } else {
      saveOriginalTransactionToHistory(transaction, "UPDATE");
    }*/

    // 2. 处理整单折扣计算
    processCommercialDiscount(context);

    // 3. 执行金额计算和价税分离
    doCalculateAmount(transaction);
  }

  @Override
  public void beforeCommitTransaction(TransactionCommitContext context) {

  }

  /**
   * 保存原始交易数据到历史表
   * <p>
   * 在折扣计算前调用，保存原始单据数据以满足审计追踪要求。
   * </p>
   *
   * @param transaction 原始交易数据
   * @param operationType 操作类型
   */
  private void saveOriginalTransactionToHistory(Transaction transaction, String operationType) {
    try {
      transactionHistoryService.saveOriginalTransaction(transaction, operationType);
    } catch (Exception e) {
      // 历史表保存失败不应阻塞主流程，仅记录警告日志
      log.warn("保存原始交易数据到历史表失败，transactionId={}, operationType={}",
              transaction.getId(), operationType, e);
    }
  }

  /**
   * 处理整单折扣
   * <p>
   * 根据越南扩展字段中的折扣参数，计算并分摊折扣金额到明细行。
   * 支持含税/不含税折扣，以及按税率段分摊或按明细行分摊两种方式。
   * </p>
   * <p>
   * <b>注意：</b>此方法只负责折扣计算和分摊，不执行参数校验。
   * 所有参数校验已在 VNTransactionValidator.validateDiscountParameters 中完成。
   * </p>
   *
   * @param transactionValidateContext 交易校验上下文
   */
  public void processCommercialDiscount(TransactionValidateContext transactionValidateContext) {
    Transaction transaction = transactionValidateContext.getTransaction();
    // 使用折扣计算服务处理整单折扣
    DiscountDistributionContext context = discountCalculationService.calculate(transaction);

    if (context == null) {
      log.debug("交易没有折扣参数或折扣计算被跳过");
      return;
    }

    // 应用折扣分摊结果
    applyDiscountDistributionResult(transaction, context);

    log.info("整单折扣处理完成，transaction:{}", JSON.toJSONString(transaction));
    TransactionBusinessRoute businessRoute = TransactionBusinessRoute.builder()
        .transactionType(TransactionType.fromCode(transaction.getTransactionType()))
        .countryCode(CountryCode.fromCode(transaction.getCountryCode()))
        .build();
    List<TransactionValidator> validators = transactionExtensionManager.getValidatorsRegistry().getPluginsFor(businessRoute);
    for (TransactionValidator validator : validators) {
      validator.validateAccumulation(transactionValidateContext, true);
    }
  }

  /**
   * 应用折扣分摊结果到交易数据
   *
   * @param transaction 交易数据
   * @param context 折扣分摊上下文
   */
  private void applyDiscountDistributionResult(Transaction transaction, DiscountDistributionContext context) {
    // 按明细行分摊时，各行折扣金额已在分摊策略中更新
    // 如果有折扣行，添加到交易明细
    if (context.getDiscountLines() != null && !context.getDiscountLines().isEmpty()) {
      List<TransactionLine> lines = transaction.getLines();
      if (!CollectionUtils.isEmpty(lines)) {
        // 为折扣行分配行号
        int maxLineNumber = lines.stream()
                .mapToInt(line -> line.getLineNumber() == null ? 0 : line.getLineNumber())
                .max()
                .orElse(0);

        for (TransactionLine discountLine : context.getDiscountLines()) {
          discountLine.setLineNumber(++maxLineNumber);
          lines.add(discountLine);
        }
      }
    }
  }

  private Function<BigDecimal, BigDecimal> quantityRounding(Transaction transaction) {
    return raw -> raw.setScale(3, RoundingMode.HALF_UP);
  }

  private Function<BigDecimal, BigDecimal> unitPriceRounding(Transaction transaction) {
    return raw -> raw.setScale(6, RoundingMode.HALF_UP);
  }

  private Function<BigDecimal, BigDecimal> amountRounding(Transaction transaction) {
    if (CurrencyCode.VND.getCode().equals(transaction.getCurrencyCode())) {
      return raw -> raw.setScale(0, RoundingMode.HALF_UP);
    } else {
      return raw -> raw.setScale(2, RoundingMode.HALF_UP);
    }
  }

  private Function<BigDecimal, BigDecimal> invoiceAmountRounding(Transaction transaction) {
    if (CurrencyCode.VND.getCode().equals(transaction.getExchangeRateCurrencyCode())) {
      return raw -> raw.setScale(0, RoundingMode.HALF_UP);
    } else {
      return raw -> raw.setScale(2, RoundingMode.HALF_UP);
    }
  }
}


path: references/VNCommonExtension.java
content: package com.baiwang.global.transaction.adaptor.vn;

import com.baiwang.xbridge3.database.core.consts.ColumnRoleType;
import com.baiwang.xbridge3.database.extfields.annotation.ColumnStandardType;
import com.baiwang.xbridge3.database.extfields.annotation.EntityColumnSchema;
import com.baiwang.xbridge3.database.extfields.model.ExtensionObjectSpec;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

/**
 * 越南交易扩展信息
 * <p>
 * 该类用于存储越南地区特有的交易相关扩展字段。
 * </p>
 *
 * @author Baiwang
 * @since 1.0
 */
@Setter
@Getter
public class VNCommonExtension {

  public static final ExtensionObjectSpec<VNCommonExtension> KEY = ExtensionObjectSpec.<VNCommonExtension>builder()
      .name("vn")
      .type(VNCommonExtension.class)
      .defaultValueSupplier(VNCommonExtension::new)
      .build();

  /**
   * 发票类型标识符，用于区分不同类型的发票
   * <p>
   * 收银机发票类型:
   * <ul>
   *   <li>{@code 01/MTT}: 收银机开具的增值税发票</li>
   *   <li>{@code 02/MTT}: 收银机开具的销售发票</li>
   * </ul>
   * <p>
   * 常规发票类型:
   * <ul>
   *   <li>{@code 01GTKT}: 常规增值税发票</li>
   *   <li>{@code 02GTTT}: 常规销售发票</li>
   * </ul>
   * </p>
   *
   * <b>约束条件:</b> 当被调整的发票为百望开具时此字段为条件必填
   *
   * @see #invoiceForm
   * @see #invoiceSeries
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Invoice Type",
      columnName = "idx_str1",
      length = 6,
      columnRole = ColumnRoleType.IndexedExtDataField,
      listShowQuery = true
  )
  private String invoiceType;

  /**
   * 发票形式编码
   * <ul>
   *   <li>{@code 1}: 增值税发票</li>
   *   <li>{@code 2}: 销售发票</li>
   * </ul>
   *
   * @see #originalInvoiceForm
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Invoice Form Type",
      columnName = "idx_str2",
      length = 1,
      columnRole = ColumnRoleType.IndexedExtDataField,
      listShowQuery = true
  )
  private String invoiceForm;

  /**
   * 发票序列号，格式如: K26MYY, C26TYY
   *
   * @see #originalInvoiceSeries
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Invoice Series",
      columnName = "idx_str3",
      length = 8,
      columnRole = ColumnRoleType.IndexedExtDataField,
      listShowQuery = true
  )
  private String invoiceSeries;

  /**
   * 发票号码
   *
   * @since 1.0
   */
  @EntityColumnSchema(
          title = "Invoice No.",
          columnName = "idx_str4",
          length = 20,
          columnRole = ColumnRoleType.IndexedExtDataField,
          listShowQuery = true
  )
  private String invoiceNo;

  /**
   * 付款方式编码
   * <ul>
   *   <li>{@code TM}: 现金</li>
   *   <li>{@code CK}: 银行转账</li>
   *   <li>{@code TM/CK}: 现金/银行转账</li>
   *   <li>{@code DTCN}: 债务抵消</li>
   *   <li>{@code KTT}: 需付款</li>
   * </ul>
   *
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Payment Method",
      columnRole = ColumnRoleType.VirtualExtDataField,
      listShowQuery = true
  )
  private String paymentMethodCode;

  /**
   * 发票类别编码
   * <ul>
   *   <li>{@code 0}: 标准发票项目</li>
   *   <li>{@code 2}: 付款/结算发票项目</li>
   * </ul>
   *
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Invoice Class",
      columnRole = ColumnRoleType.VirtualExtDataField
  )
  private String invoiceClass;

  /**
   * 购买方全称，若发票用于报销则需填写企业全称
   *
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Full Buyer Name",
      columnRole = ColumnRoleType.VirtualExtDataField
  )
  private String fullBuyerName;

  /**
   * 预算关系代码，用于财务预算管理
   *
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Budget Relation Id",
      columnRole = ColumnRoleType.VirtualExtDataField
  )
  private String budgetRelationId;

  /**
   * 购买方证件类型编码
   * <ul>
   *   <li>{@code 1}: 身份证</li>
   *   <li>{@code 2}: 护照</li>
   * </ul>
   *
   * @see #buyerIdNo
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Buyer Id Type",
      columnRole = ColumnRoleType.VirtualExtDataField
  )
  private String buyerIdType;

  /**
   * 购买方证件号码
   * <p>
   * 当 [buyerIdType](file:///D:\Workspace\einvoice\global-document-engine\document-engine-api\src\main\java\com\baiwang\global\transaction\adaptor\vn\VNTransactionExtension.java#L180-L180) 有值时此字段必填
   * </p>
   *
   * @see #buyerIdType
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Buyer Id No.",
      columnRole = ColumnRoleType.VirtualExtDataField
  )
  private String buyerIdNo;

  /**
   * 原发票形式 - 调整发票时使用
   * <p>
   * 条件必填：当被调整的发票为百望开具时
   * </p>
   *
   * @see #invoiceForm
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Original Invoice Form",
      columnRole = ColumnRoleType.VirtualExtDataField
  )
  private String originalInvoiceForm;

  /**
   * 原发票号码 - 调整发票时使用
   * <p>
   * 条件必填：当被调整的发票为百望开具时
   * </p>
   *
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Original Invoice No.",
      columnRole = ColumnRoleType.VirtualExtDataField
  )
  private String originalInvoiceNo;

  /**
   * 原发票序列号 - 调整发票时使用
   * <p>
   * 示例：K26MYY或C26TYY
   * 条件必填：当被调整的发票为百望开具时
   * </p>
   *
   * @see #invoiceSeries
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Original Invoice Series",
      columnRole = ColumnRoleType.VirtualExtDataField
  )
  private String originalInvoiceSeries;

  /**
   * 原发票开票日期 - 调整发票时使用
   * <p>
   * 格式：ISO 8601日期格式
   * 条件必填：当被调整的发票为百望开具时
   * </p>
   *
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Original Issue Date",
      columnRole = ColumnRoleType.VirtualExtDataField
  )
  private String originalIssueDate;

  /**
   * 使用交易货币开票标志
   * <ul>
   *   <li>{@code false}: 使用本币开票</li>
   *   <li>{@code true}: 使用交易货币开票</li>
   * </ul>
   * <p>
   * 默认值为false，即使用转换后的本币开票
   * </p>
   *
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Use Exchange Issue Flag",
      columnRole = ColumnRoleType.VirtualExtDataField
  )
  private Boolean useExchangeIssueFlag;

  /**
   * 调整或替换原因
   * <p>
   * 记录发票调整或替换的具体原因说明
   * </p>
   *
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Adjust Reason",
      columnRole = ColumnRoleType.VirtualExtDataField
  )
  private String adjustReason;

  /**
   * 校验码
   * <p>
   * 对应越南 MCQT
   * </p>
   *
   * @since 1.0
   */
  @EntityColumnSchema(
      title = "Check Code",
      columnRole = ColumnRoleType.VirtualExtDataField
  )
  private String checkCode;


  /**
   *
   *
   * 跟 FPT 交互防重的唯一ID,  UUID
   *  transactionUuid 与 invoiceSid 一一对应
   *  交易入库时 生成 invoiceSid
   * <p>
   * 对应越南 sid
   * </p>
   *
   * @since 1.0
   */
  @EntityColumnSchema(
          title = "Invoice Sid",
          columnRole = ColumnRoleType.VirtualExtDataField
  )
  private String invoiceSid;

  // ==================== 整单折扣相关字段 ====================

  /**
   * 整单折扣金额（正数）
   * <p>
   * 与 {@link #commercialDiscountRate} 互斥，二者只能选其一
   * 当 discountTaxIncluded="1" 时，此金额为含税金额
   * 当 discountTaxIncluded="0" 时，此金额为不含税金额
   * </p>
   * <p>
   * <b>约束条件:</b>
   * <ul>
   *   <li>必须 ≤ 发票总金额</li>
   *   <li>精度：小数点后≤6位</li>
   *   <li>与 commercialDiscountRate 互斥</li>
   * </ul>
   * </p>
   *
   * @since 1.2
   */
  @EntityColumnSchema(
      title = "Commercial Discount Amount",
      columnName = "idx_number1",
      length = 20,
      scale = 6,
      columnRole = ColumnRoleType.IndexedExtDataField
  )
  private java.math.BigDecimal commercialDiscount;

  /**
   * 整单折扣比例 (0,1)
   * <p>
   * 与 {@link #commercialDiscount} 互斥，二者只能选其一
   * 示例: 0.10 表示10%折扣
   * </p>
   * <p>
   * <b>约束条件:</b>
   * <ul>
   *   <li>必须在 (0,1) 范围内，不包含0和1</li>
   *   <li>精度：小数点后≤4位</li>
   *   <li>与 commercialDiscount 互斥</li>
   * </ul>
   * </p>
   *
   * @since 1.2
   */
  @EntityColumnSchema(
      title = "Commercial Discount Rate",
      columnName = "idx_number2",
      length = 10,
      scale = 4,
      columnRole = ColumnRoleType.IndexedExtDataField
  )
  private java.math.BigDecimal commercialDiscountRate;

  /**
   * 折扣是否含税
   * <p>
   * "1"=含税折扣：折扣金额为含税金额，系统需进行价税分离
   * "0"=不含税折扣：折扣金额为不含税金额，系统需计算税额
   * </p>
   * <p>
   * <b>约束条件:</b> 当 commercialDiscount 或 commercialDiscountRate 有值时必填
   * </p>
   *
   * @since 1.2
   */
  @EntityColumnSchema(
      title = "Discount Tax Included",
      columnName = "idx_str5",
      length = 1,
      columnRole = ColumnRoleType.IndexedExtDataField
  )
  private String discountTaxIncluded;

  /**
   * 折扣分摊方式
   * <p>
   * 定义整单折扣如何在发票明细行之间进行分摊。
   * 用户传入数值代码指定分摊方式。
   * </p>
   * <p>
   * <b>枚举值:</b>
   * <ul>
   *   <li>{@code 0}: 按税率段汇总分摊(BY_TAX_RATE)，生成独立CK折扣行(lineType=1)</li>
   *   <li>{@code 1}: 按明细行逐行分摊(BY_LINE_ITEM)，折扣内嵌明细行</li>
   * </ul>
   * </p>
   * <p>
   * <b>约束条件:</b> 当 commercialDiscount 或 commercialDiscountRate 有值时必填
   * </p>
   *
   * @since 1.2
   */
  @EntityColumnSchema(
      title = "Commercial Discount Method",
      columnName = "idx_str6",
      length = 50,
      columnRole = ColumnRoleType.IndexedExtDataField
  )
  private String commercialDiscountMethod;

  @EntityColumnSchema(
      title = "Commercial Discount itemCodes",
      columnRole = ColumnRoleType.VirtualExtDataField,
      type = ColumnStandardType.EXT_JSON
  )
  private List<String> commercialDiscountItemCodes;
}


path: references/ExtensionUtils.java
content: package com.baiwang.global.transaction.utils;

import com.baiwang.xbridge3.common.json.JacksonObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class ExtensionUtils {
    public static <T> T getExtension(String key, Class<T> type,ObjectNode extensions) {
        if (extensions == null) {
            return null;
        }
        JsonNode value = extensions.path(key);
        if (!value.isObject()) {
            return null;
        }
        return JacksonObjectMapper.get().toJavaObject(value, type);
    }

}


path: references/xbridge-ext-use.md
content: # 交易表扩展字段使用指南（深入版）

## 背景

本文档基于 `gbw_transaction` 表和 `gbw_transaction_ext` 表的代码实现，深入分析 xbridge3 框架中扩展字段的工作原理，包括 `@EntityColumnSchema` 注解详解、复杂类型配置、以及框架如何自动处理扩展字段的增删改查。

---

## 一、数据库表结构设计

### 1.1 主表与扩展表设计

**主表（gbw_transaction）**: 存储交易核心业务数据 + 索引字段
```sql
CREATE TABLE gbw_transaction (
    id BIGINT PRIMARY KEY,
    -- ... 业务字段
    -- 字符索引字段（idx_str1 - idx_str15）
    idx_str1 VARCHAR(100) COMMENT '字符索引1',
    -- ...
    -- 日期索引字段（idx_date1 - idx_date5）
    idx_date1 DATETIME COMMENT '日期索引1',
    -- ...
    -- 数值索引字段（idx_number1 - idx_number10）
    idx_number1 DECIMAL(35, 13) COMMENT '数值索引1'
    -- ...
);
```

**扩展表（gbw_transaction_ext）**: 存储JSON格式的扩展数据
```sql
CREATE TABLE gbw_transaction_ext (
    transaction_id BIGINT PRIMARY KEY COMMENT '主表ID外键',
    tenant_id BIGINT NOT NULL COMMENT '租户ID',
    transaction_date DATETIME COMMENT '交易日期',
    ext_data JSON COMMENT '扩展数据JSON格式'
);
```

### 1.2 索引字段分配

| 索引字段 | 类型 | 数量 | 适用场景 |
|---------|------|------|---------|
| idx_str1 ~ idx_str15 | VARCHAR(100) | 15个 | 字符串类型扩展字段（如发票类型、发票序列号）|
| idx_date1 ~ idx_date5 | DATETIME | 5个 | 日期类型扩展字段 |
| idx_number1 ~ idx_number10 | DECIMAL(35,13) | 10个 | 数值类型扩展字段（如折扣金额、折扣比例）|

---

## 二、xbridge3 框架扩展字段核心机制

### 2.1 核心组件关系图

```
┌─────────────────────────────────────────────────────────────────┐
│                    EntityTableSchemaManager                      │
│  (管理所有实体表的Schema配置，包括扩展字段的解析和查询)              │
└──────────────────────────┬──────────────────────────────────────┘
                           │
          ┌────────────────┼────────────────┐
          │                │                │
          ▼                ▼                ▼
┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ EntitySchema    │ │ EntitySchema │ │ EntitySchema     │
│ (Transaction)   │ │ (Transaction│ │ (TransactionLine)│
│                 │ │ Ext)         │ │                  │
└────────┬────────┘ └──────────────┘ └────────┬─────────┘
         │                                    │
         │         ┌──────────────────────┐   │
         │         │ Extension Schema     │   │
         │         │ (VN/MY Extension)    │   │
         │         └──────────────────────┘   │
         │                                    │
         ▼                                    ▼
┌─────────────────┐                 ┌──────────────────┐
│ @EntityColumn   │                 │ @EntityColumn    │
│ Schema          │                 │ Schema           │
│ (VirtualExtData)│                 │ (VirtualExtData) │
└─────────────────┘                 └──────────────────┘
```

### 2.2 Schema 配置初始化流程

**全局 Schema 配置**（`DocumentEngineConfiguration.java`）:

```java
@Bean
public EntityTableSchemaManager entityTableSchemaManager(
        EntityTableFactoryManager tableFactoryManager,
        ObjectProvider<EntityTableSchemaConfigProvider> schemaConfigProviders) {
    
    // 1. 解析主实体类（基础Schema）
    EntityTableSchemaConfig globalTableSchema = EntitySchemaAnnotationParser.parseEntity(
        Transaction.class,        // 主表
        TransactionExt.class,     // 扩展表
        TransactionLine.class,    // 明细表
        TransactionLineExt.class, // 明细扩展表
        TransactionLineAddress.class
    );
    
    EntityTableSchemaManager.EntityTableSchemaManagerBuilder schemaManagerBuilder = 
        EntityTableSchemaManager.builder()
            .entityTableFactoryManager(tableFactoryManager)
            .globalSchemaConfig(globalTableSchema);
    
    // 2. 注册各国特定的扩展Schema（通过SPI机制）
    for (EntityTableSchemaConfigProvider configProvider : schemaConfigProviders) {
        EntityTableSchemaConfigItem configItem = configProvider.getSchemaConfig();
        schemaManagerBuilder.entityTableSchemaConfig(
            configItem.getTableSchemaID(), 
            configItem.getTableSchemaConfig()
        );
    }
    
    return schemaManagerBuilder.build();
}
```

**国家特定 Schema 配置**（`VNTransactionProcessor.java`）:

```java
@Component
public class VNTransactionProcessor implements 
        TransactionProcessor, 
        EntityTableSchemaConfigProvider {  // 实现Schema配置提供者接口

    @Override
    public EntityTableSchemaConfigItem getSchemaConfig() {
        // 解析越南扩展字段类
        EntityTableSchemaConfig vnTransactionSchema = EntitySchemaAnnotationParser.parseExtension(
            VNTransactionExtension.class,      // 主表扩展
            VNTransactionLineExtension.class   // 明细表扩展
        );
        
        return EntityTableSchemaConfigItem.builder()
            .tableSchemaID(EntityTableSchemaID.parse("global.VN"))  // Schema标识
            .tableSchemaConfig(vnTransactionSchema)
            .build();
    }
}
```

### 2.3 扩展类定义规范

**VNTransactionExtension.java**:
```java
@EntityExtensionSchema(
    tableId = "gbw_transaction",    // 关联的主表
    extNamespace = "vn"              // JSON中的命名空间前缀
)
@Data
public class VNTransactionExtension extends VNCommonExtension {
    // 可以在此添加交易表特定的扩展字段
}
```

**VNTransactionLineExtension.java**:
```java
@EntityExtensionSchema(
    tableId = "gbw_transaction_line",  // 关联的明细表
    extNamespace = "vn"
)
@Data
public class VNTransactionLineExtension extends VNCommonLineExtension {
}
```

---

## 三、@EntityColumnSchema 注解详解

### 3.1 注解属性全解析

```java
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EntityColumnSchema {
    String title();                    // 字段标题（必填）- 用于元数据展示
    
    ColumnStandardType type() default ColumnStandardType.AUTO;  // 字段标准类型
    
    String dictCode() default "";      // 字典代码 - 用于下拉选项
    
    String defaultValue() default "";  // 默认值
    
    boolean nullable() default true;   // 是否可空
    
    int length() default -1;           // 最大长度（字符串类型）
    
    int scale() default -1;            // 小数位数（数值类型）
    
    ColumnRoleType columnRole() default ColumnRoleType.Physical; // 列角色类型（核心）
    
    String targetTable() default "";   // 目标表名（用于关联）
    
    String targetForeignKeyColumn() default "";  // 目标表外键列
    
    String sourceReferencedColumn() default "";  // 当前表引用列
    
    String extDataColumn() default ""; // 扩展数据JSON列名
    
    String columnName() default "";    // 索引列名（用于IndexedExtDataField映射）
    
    boolean listShowQuery() default false;       // 列表查询时是否作为查询条件
    
    ColumnQueryMode listQueryMode() default ColumnQueryMode.SINGLE; // 查询模式
}
```

### 3.2 ColumnStandardType 支持的类型

```java
public enum ColumnStandardType {
    AUTO,           // 自动推断（根据Java类型自动判断）
    EXT_JSON,       // 扩展JSON字段（用于extensions ObjectNode）
    ID_LONG,        // 长整型ID（主键）
    INSTANT_TIME,   // Instant时间类型
    BOOLEAN,        // 布尔类型
    DECIMAL,        // 大数字（BigDecimal）
    INTEGER,        // 整型
    LONG_INT,       // 长整型
    STRING,         // 字符串
    ONE_TO_MANY,    // 一对多关联（用于lines等关联字段）
}
```

**类型自动推断规则**:
```java
public static ColumnStandardType fromJavaType(Class<?> type) {
    if (ObjectNode.class.isAssignableFrom(type)) return EXT_JSON;
    if (Instant.class.isAssignableFrom(type)) return INSTANT_TIME;
    if (Boolean.class.isAssignableFrom(type) || boolean.class.equals(type)) return BOOLEAN;
    if (BigDecimal.class.isAssignableFrom(type)) return DECIMAL;
    if (Integer.class.isAssignableFrom(type) || int.class.equals(type)) return INTEGER;
    if (Long.class.isAssignableFrom(type) || long.class.equals(type)) return LONG_INT;
    if (String.class.isAssignableFrom(type)) return STRING;
    return AUTO;
}
```

### 3.3 ColumnRoleType 列角色类型详解

```java
public enum ColumnRoleType {
    Physical(false),              // 物理字段 - 数据库实际存在的列（如transaction_uuid）
    ExtDataIndex(false),          // 扩展字段索引列 - 如 idx_str1, idx_date1 等
    ExtData(false),               // 扩展字段数据列 - ext_data JSON列
    VirtualExtData(true),         // 虚拟扩展字段 - 代表整个extensions ObjectNode
    VirtualExtDataField(true),    // 虚拟扩展字段值 - 仅存储在JSON中，不映射索引
    IndexedExtDataField(false);   // 带索引的扩展字段 - 同时存储在JSON和索引列

    private final boolean virtualColumn;  // 是否为虚拟列（不实际存储）
}
```

**角色类型对比表**:

| 角色类型 | 是否虚拟列 | 存储位置 | 查询方式 | 用途 |
|---------|-----------|---------|---------|------|
| Physical | 否 | 主表物理列 | 直接字段查询 | 普通业务字段 |
| ExtDataIndex | 否 | 主表索引列 | 直接字段查询 | 扩展字段的索引列 |
| ExtData | 否 | 扩展表ext_data | JSON路径查询 | JSON存储 |
| VirtualExtData | 是 | 内存/JSON | JSON路径查询 | 代表整个extensions对象 |
| VirtualExtDataField | 是 | JSON | JSON路径查询 | 仅JSON存储的字段（不常查询） |
| IndexedExtDataField | 否 | JSON + 索引列 | 索引字段查询 | 需要索引的扩展字段 |

---

## 四、实体类完整配置示例

### 4.1 主表实体类（Transaction.java）

```java
@Data
@EqualsAndHashCode(callSuper = false)
@TableName(value = "gbw_transaction")
@EntityTableSchema(title = "Transaction")
public class Transaction extends ExtensiableSchemaEntityObject implements ChangeAuditableObject {

    // ========== 1. 主表业务字段（Physical） ==========
    @TableId(value = "id")
    @EntityColumnSchema(title = "{gbw_transaction.id}", type = ColumnStandardType.ID_LONG)
    private Long id;
    
    @TableField(value = "transaction_uuid")
    @EntityColumnSchema(title = "Transaction UUID", length = 50)
    private String transactionUuid;
    
    @TableField(value = "transaction_type")
    @EntityColumnSchema(title = "Transaction Type", length = 10)
    private String transactionType;
    
    // ... 其他业务字段
    
    // ========== 2. 索引字段（ExtDataIndex） ==========
    // 这些字段用于存储需要索引的扩展字段值
    
    @TableField(value = "idx_str1")
    @EntityColumnSchema(title = "Index String 1", length = 100, columnRole = ColumnRoleType.ExtDataIndex)
    private String idxStr1;
    
    @TableField(value = "idx_str2")
    @EntityColumnSchema(title = "Index String 2", length = 100, columnRole = ColumnRoleType.ExtDataIndex)
    private String idxStr2;
    
    @TableField(value = "idx_number1")
    @EntityColumnSchema(title = "Index Number 1", length = 35, scale = 13, columnRole = ColumnRoleType.ExtDataIndex)
    private BigDecimal idxNumber1;
    
    // ... idx_str3-15, idx_date1-5, idx_number2-10
    
    // ========== 3. 扩展字段（VirtualExtData） ==========
    // 这是整个扩展字段的容器，对应 ext_data JSON
    
    @TableField(exist = false)  // 不直接映射数据库列
    @EntityColumnSchema(
        title = "Extensions",
        type = ColumnStandardType.EXT_JSON,
        columnRole = ColumnRoleType.VirtualExtData,      // 虚拟扩展字段
        targetTable = "gbw_transaction_ext",             // 扩展表名
        sourceReferencedColumn = "id",                    // 主表关联列
        targetForeignKeyColumn = "transaction_id",        // 扩展表外键列
        extDataColumn = "ext_data"                        // 扩展数据JSON列
    )
    private ObjectNode extensions;
    
    // ========== 4. 关联字段（ONE_TO_MANY） ==========
    
    @TableField(exist = false)
    @EntityColumnSchema(
        title = "Transaction lines",
        type = ColumnStandardType.ONE_TO_MANY,
        targetTable = "gbw_transaction_line",
        targetForeignKeyColumn = "transaction_id",
        sourceReferencedColumn = "id"
    )
    private List<TransactionLine> lines;
    
    // ========== 5. 扩展字段操作方法 ==========
    
    /**
     * 从数据库加载扩展字段
     */
    public void loadExtensions() {
        TransactionExt transactionExt = TransactionExtMapper.INSTANCE.get().selectById(getId());
        if (transactionExt != null) {
            setExtensions(transactionExt.getExtData());  // String -> ObjectNode
        }
    }
    
    /**
     * 获取指定key的扩展对象（基类方法）
     */
    public <T> T getExtension(String extKey, Class<T> type) {
        if (getExtensions() == null) {
            return null;
        }
        JsonNode value = getExtensions().path(extKey);
        if (!value.isObject()) {
            return null;
        }
        return JacksonObjectMapper.get().toJavaObject(value, type);
    }
}
```

### 4.2 扩展表实体类（TransactionExt.java）

```java
@Data
@TableName(value = "gbw_transaction_ext")
@EntityTableSchema(title = "Transaction Extensions")
public class TransactionExt {

    @TableId(value = "transaction_id")
    @EntityColumnSchema(title = "Transaction ID", type = ColumnStandardType.LONG_INT)
    private Long transactionId;  // 主键同时也是外键

    @TableField(value = "tenant_id")
    @EntityColumnSchema(title = "Tenant ID")
    private Long tenantId;

    @TableField(value = "transaction_date")
    @EntityColumnSchema(title = "Transaction Date")
    private Instant transactionDate;

    @TableField(value = "ext_data")
    @EntityColumnSchema(title = "Extension Data", type = ColumnStandardType.STRING)
    private String extData;  // JSON字符串存储

    public void onCreateSetup(Transaction transaction) {
        setTransactionId(transaction.getId());
        setTenantId(transaction.getTenantId());
        setTransactionDate(transaction.getTransactionDate());
    }
}
```

---

## 五、国家扩展字段定义详解

### 5.1 VNCommonExtension.java（越南扩展字段）

```java
@Setter
@Getter
public class VNCommonExtension {

    // ===== 定义ExtensionObjectSpec（用于类型转换）=====
    public static final ExtensionObjectSpec<VNCommonExtension> KEY = 
        ExtensionObjectSpec.<VNCommonExtension>builder()
            .name("vn")                              // JSON中的key
            .type(VNCommonExtension.class)           // 对应的Java类型
            .defaultValueSupplier(VNCommonExtension::new)
            .build();

    // ========== 1. 带索引的扩展字段（IndexedExtDataField）==========
    // 这些字段会同时存储在 ext_data JSON 和主表的 idx_str/idx_number 字段中
    
    /**
     * 发票类型 - 映射到 idx_str1
     * 常用于列表查询，所以需要索引
     */
    @EntityColumnSchema(
        title = "Invoice Type",
        columnName = "idx_str1",                    // 映射到索引字段
        length = 6,
        columnRole = ColumnRoleType.IndexedExtDataField,
        listShowQuery = true                        // 列表查询条件
    )
    private String invoiceType;

    @EntityColumnSchema(
        title = "Invoice Form Type",
        columnName = "idx_str2",
        length = 1,
        columnRole = ColumnRoleType.IndexedExtDataField
    )
    private String invoiceForm;

    @EntityColumnSchema(
        title = "Invoice Series",
        columnName = "idx_str3",
        length = 8,
        columnRole = ColumnRoleType.IndexedExtDataField,
        listShowQuery = true
    )
    private String invoiceSeries;

    @EntityColumnSchema(
        title = "Invoice No.",
        columnName = "idx_str4",
        length = 20,
        columnRole = ColumnRoleType.IndexedExtDataField,
        listShowQuery = true
    )
    private String invoiceNo;

    // 整单折扣相关字段 - 映射到数值索引
    @EntityColumnSchema(
        title = "Commercial Discount Amount",
        columnName = "idx_number1",                 // 映射到数值索引
        length = 20,
        scale = 6,
        columnRole = ColumnRoleType.IndexedExtDataField
    )
    private BigDecimal commercialDiscount;

    @EntityColumnSchema(
        title = "Commercial Discount Rate",
        columnName = "idx_number2",
        length = 10,
        scale = 4,
        columnRole = ColumnRoleType.IndexedExtDataField
    )
    private BigDecimal commercialDiscountRate;

    // ========== 2. 仅JSON存储的扩展字段（VirtualExtDataField）==========
    // 这些字段只存储在 ext_data JSON 中，不占用索引字段
    
    @EntityColumnSchema(
        title = "Payment Method",
        columnRole = ColumnRoleType.VirtualExtDataField,  // 仅JSON存储
        listShowQuery = true
    )
    private String paymentMethodCode;

    @EntityColumnSchema(
        title = "Invoice Class",
        columnRole = ColumnRoleType.VirtualExtDataField
    )
    private String invoiceClass;

    @EntityColumnSchema(
        title = "Full Buyer Name",
        columnRole = ColumnRoleType.VirtualExtDataField
    )
    private String fullBuyerName;

    // ========== 3. 复杂类型字段（List、嵌套对象）==========
    
    /**
     * 折扣适用的商品代码列表 - 存储为JSON数组
     */
    @EntityColumnSchema(
        title = "Commercial Discount itemCodes",
        columnRole = ColumnRoleType.VirtualExtDataField,
        type = ColumnStandardType.EXT_JSON  // 复杂类型使用EXT_JSON
    )
    private List<String> commercialDiscountItemCodes;
}
```

### 5.2 存储的JSON结构示例

```json
{
    "vn": {
        // 带索引的字段（同时存储在 idx_str1-6, idx_number1-2）
        "invoiceType": "01/MTT",
        "invoiceForm": "1",
        "invoiceSeries": "K26MYY",
        "invoiceNo": "0000123",
        "discountTaxIncluded": "1",
        "commercialDiscountMethod": "0",
        "commercialDiscount": 1000.00,
        "commercialDiscountRate": 0.10,
        
        // 仅JSON存储的字段
        "paymentMethodCode": "TM",
        "invoiceClass": "0",
        "fullBuyerName": "Công ty TNHH ABC",
        
        // 复杂类型字段
        "commercialDiscountItemCodes": ["ITEM001", "ITEM002", "ITEM003"],
        
        // 其他虚拟字段
        "buyerIdType": "1",
        "buyerIdNo": "123456789",
        "adjustReason": "调整原因说明"
    }
}
```

---

## 六、扩展字段增删改查详解

### 6.1 创建交易时保存扩展字段

**关键点**: 当前框架不会自动处理扩展表插入，需要应用层手动处理。

```java
@Repository
@RequiredArgsConstructor
public class TransactionDAO {
    private final TransactionMapper transactionMapper;
    private final TransactionExtMapper transactionExtMapper;
    private final TransactionLineMapper transactionLineMapper;
    private final TransactionLineExtMapper transactionLineExtMapper;

    public Transaction createTransaction(Transaction transaction) {
        // 1. 获取EntityObjectManager（带Schema支持）
        EntityTableSchemaID tableSchemaID = transaction.getTableSchemaID();
        EntityObjectManager<Transaction> transactionEntityManager = 
            transactionMapper.getEntityObjectManager(tableSchemaID);
        
        // 2. 插入主表数据
        // 注意：这里只插入主表，不会自动处理扩展表
        transactionEntityManager.insert(transaction);
        
        // 3. 插入明细行数据
        if (!CollectionUtils.isEmpty(transaction.getLines())) {
            transaction.getLines().forEach(line -> line.onCreateSetup(transaction));
            transactionLineEntityManager.insertMultiple(transaction.getLines());
            
            // 4. 插入明细行扩展字段
            for (TransactionLine line : transaction.getLines()) {
                insertTransactionLineExt(line);
            }
        }
        
        // 5. 【重要】手动保存扩展字段到扩展表
        TransactionExt transactionExt = new TransactionExt();
        transactionExt.onCreateSetup(transaction);
        // ObjectNode -> JSON字符串
        transactionExt.setExtData(transaction.getExtensions().toString());
        transactionExtMapper.insert(transactionExt);
        
        return transaction;
    }
    
    private void insertTransactionLineExt(TransactionLine line) {
        TransactionLineExt lineExt = new TransactionLineExt();
        lineExt.onCreateSetup(line);
        ObjectNode extensions = line.getExtensions();
        if (extensions == null) {
            extensions = JacksonObjectMapper.get().createObjectNode();
        }
        lineExt.setExtData(extensions.toString());
        transactionLineExtMapper.insert(lineExt);
    }
}
```

### 6.2 更新交易时更新扩展字段

```java
public boolean updateTransaction(Transaction transaction, Collection<TransactionStatus> expectedStatus) {
    EntityTableSchemaID tableSchemaID = transaction.getTableSchemaID();
    EntityObjectManager<Transaction> transactionEntityManager = 
        transactionMapper.getEntityObjectManager(tableSchemaID);
    
    // 1. 构建查询条件（乐观锁）
    EntityQueryWrapper queryWrapper = EntityQueryWrapper.columnNameQuery(
        transactionEntityManager.getEntityTable()
    );
    queryWrapper.eq("id", transaction.getId());
    if (!expectedStatus.isEmpty()) {
        queryWrapper.in("transaction_status", 
            expectedStatus.stream().map(TransactionStatus::getStatus).toList()
        );
    }
    
    // 2. 更新主表数据
    int updated = transactionEntityManager.updateSelective(transaction, queryWrapper);
    if (updated <= 0) {
        return false;
    }
    
    // 3. 【重要】手动更新扩展字段
    TransactionExt transactionExt = new TransactionExt();
    transactionExt.setTransactionId(transaction.getId());
    transactionExt.setExtData(transaction.getExtensions().toString());
    transactionExtMapper.updateById(transactionExt);
    
    // 4. 更新明细行及扩展字段
    List<TransactionLine> existsLines = transactionLineMapper.selectList(
        Wrappers.lambdaQuery(TransactionLine.class)
            .eq(TransactionLine::getTransactionId, transaction.getId())
    );
    Map<Long, TransactionLine> toRemoveLinesMap = existsLines.stream()
        .collect(Collectors.toMap(TransactionLine::getId, Function.identity()));
    
    if (!CollectionUtils.isEmpty(transaction.getLines())) {
        for (TransactionLine line : transaction.getLines()) {
            TransactionLine existsLine = toRemoveLinesMap.remove(line.getId());
            if (existsLine != null) {
                // 更新现有明细行
                line.onUpdateSetup(transaction);
                transactionLineEntityManager.updateByIdSelective(line);
                
                // 更新明细行扩展字段
                TransactionLineExt lineExt = new TransactionLineExt();
                lineExt.setLineId(line.getId());
                lineExt.setExtData(line.getExtensions().toString());
                transactionLineExtMapper.updateById(lineExt);
            } else {
                // 新增明细行
                line.onCreateSetup(transaction);
                transactionLineEntityManager.insertSelective(line);
                insertTransactionLineExt(line);
            }
        }
        
        // 删除被移除的明细行
        List<Long> toRemoveLineIdList = toRemoveLinesMap.values().stream()
            .map(TransactionLine::getId)
            .toList();
        transactionLineMapper.deleteByIds(toRemoveLineIdList);
        transactionLineExtMapper.deleteByIds(toRemoveLineIdList);
    }
    
    return true;
}
```

### 6.3 查询时加载扩展字段

```java
@Service
public class TransactionServiceImpl implements TransactionService {
    
    public TransactionDTO queryTransactionDetail(Long transactionId) {
        // 1. 查询主表数据
        Transaction transaction = transactionMapper.selectById(transactionId);
        if (transaction == null) {
            throw new BusinessException("交易不存在");
        }
        
        // 2. 【重要】手动加载扩展字段
        transaction.loadExtensions();
        
        // 3. 加载明细行
        transaction.loadLines();
        
        // 4. 加载明细行扩展字段
        for (TransactionLine line : transaction.getLines()) {
            line.loadExtensions();
        }
        
        // 5. 转换为DTO
        return transactionConverter.convertToDTO(transaction);
    }
}
```

**Transaction.loadExtensions() 实现**:
```java
public void loadExtensions() {
    TransactionExt transactionExt = TransactionExtMapper.INSTANCE.get().selectById(getId());
    if (transactionExt != null && transactionExt.getExtData() != null) {
        // String -> ObjectNode 转换
        setExtensions(transactionExt.getExtData());
    }
}
```

**ExtensiableSchemaEntityObject.setExtensions(String) 基类实现**:
```java
public void setExtensions(String extensions) {
    JsonNode jsonNode = JacksonObjectMapper.get().toJsonNode(extensions);
    if (jsonNode instanceof ObjectNode objectNode) {
        setExtensions(objectNode);
    }
}
```

---

## 七、Converter层实现

### 7.1 TransactionConverter.java

```java
@Component
public class TransactionConverter {

    /**
     * DTO -> Entity
     */
    public Transaction convertToEntity(TransactionDTO source) {
        Transaction target = new Transaction();
        
        // 1. 基础字段映射
        target.setId(source.getId());
        target.setTransactionUuid(source.getTransactionUuid());
        target.setTransactionType(
            Optional.ofNullable(source.getTransactionType())
                .map(TransactionType::getCode)
                .orElse(null)
        );
        // ... 其他字段映射
        
        // 2. 【重要】扩展字段直接透传
        // extensions ObjectNode 直接复制，无需逐个字段转换
        target.setExtensions(source.getExtensions());
        
        // 3. 转换明细行
        List<TransactionLine> lines = convertLinesToEntities(source.getLines());
        target.setLines(lines);
        
        return target;
    }

    /**
     * Entity -> DTO
     */
    public TransactionDTO convertToDTO(Transaction source) {
        TransactionDTO target = new TransactionDTO();
        
        // 1. 基础字段映射
        target.setId(source.getId());
        target.setTransactionUuid(source.getTransactionUuid());
        target.setTransactionType(
            Optional.ofNullable(source.getTransactionType())
                .map(TransactionType::fromCode)
                .orElse(null)
        );
        // ... 其他字段映射
        
        // 2. 【重要】扩展字段直接透传
        target.setExtensions(source.getExtensions());
        
        // 3. 转换明细行
        List<TransactionLineDTO> lineDTOS = convertLinesToDTOs(source.getLines());
        target.setLines(lineDTOS);
        
        return target;
    }
    
    private TransactionLine convertLineToEntity(TransactionLineDTO source) {
        TransactionLine target = new TransactionLine();
        // ... 基础字段映射
        
        // 扩展字段直接透传
        target.setExtensions(source.getExtensions());
        
        return target;
    }
    
    private TransactionLineDTO convertLineToDTO(TransactionLine source) {
        TransactionLineDTO target = new TransactionLineDTO();
        // ... 基础字段映射
        
        // 扩展字段直接透传
        target.setExtensions(source.getExtensions());
        
        return target;
    }
}
```

---

## 八、扩展字段工具类使用

### 8.1 ExtensionUtils.java

```java
public class ExtensionUtils {
    
    /**
     * 从extensions ObjectNode中获取指定key的扩展对象
     * 
     * @param key 扩展字段key（如 "vn", "my"）
     * @param type 目标类型Class
     * @param extensions extensions ObjectNode
     * @return 转换后的对象，如果不存在返回null
     */
    public static <T> T getExtension(String key, Class<T> type, ObjectNode extensions) {
        if (extensions == null) {
            return null;
        }
        JsonNode value = extensions.path(key);
        if (!value.isObject()) {
            return null;
        }
        return JacksonObjectMapper.get().toJavaObject(value, type);
    }
}
```

### 8.2 使用示例

**读取扩展字段**:
```java
// 在Validator中使用
@Component
public class VNTransactionValidator implements TransactionValidator {
    
    @Override
    public void validateBeforeProcess(TransactionValidateContext context) {
        Transaction transaction = context.getTransaction();
        
        // 获取越南扩展字段
        VNCommonExtension vnExtension = ExtensionUtils.getExtension(
            "vn", 
            VNCommonExtension.class,
            transaction.getExtensions()
        );
        
        if (vnExtension != null) {
            // 校验发票类型
            if (StringUtils.isBlank(vnExtension.getInvoiceType())) {
                context.reject(ErrorCodes.TA0017, "invoiceType");
            }
            
            // 校验折扣参数
            validateDiscountParameters(context, transaction, vnExtension);
        }
    }
}
```

**设置扩展字段**:
```java
// 在Processor中使用
@Component
public class VNScanInvoiceProcessor implements ScanInvoiceProcessor {
    
    @Override
    public void preProcess(TransactionDTO transactionDTO) {
        ObjectNode extensions = transactionDTO.getExtensions();
        if (extensions == null) {
            extensions = JacksonObjectMapper.get().createObjectNode();
        }
        
        // 获取或创建越南扩展对象
        VNCommonExtension vnExtension = ExtensionUtils.getExtension(
            "vn", VNCommonExtension.class, extensions
        );
        if (vnExtension == null) {
            vnExtension = new VNCommonExtension();
        }
        
        // 设置扩展字段值
        vnExtension.setInvoiceType("01/MTT");
        vnExtension.setInvoiceForm("1");
        vnExtension.setPaymentMethodCode("TM");
        
        // 保存回extensions
        String jsonString = JacksonObjectMapper.get().toJsonString(vnExtension);
        JsonNode vnNode = JacksonObjectMapper.get().toJsonNode(jsonString);
        extensions.set("vn", vnNode);
        
        transactionDTO.setExtensions(extensions);
    }
}
```

---

## 九、框架自动支持的功能

### 9.1 索引字段自动同步

**xbridge3 框架自动支持以下功能**：

1. **插入/更新时自动同步**: `EntityObjectManager.insert()` / `update()` 会自动提取 `IndexedExtDataField` 并设置到对应的索引字段（idx_str1, idx_number1等）

2. **查询时自动转换**: `EntityTableSchemaManager` 在构建查询条件时，会将 `VirtualExtDataField` 的查询转换为索引字段查询或JSON路径查询

3. **asEntityDataObject 转换**: 该方法会自动处理扩展字段到索引字段的映射逻辑

4. **扩展字段自动校验**: 框架支持基于 `@EntityColumnSchema` 注解的自动校验（如长度、非空等）

**示例：框架自动同步索引字段**
```java
// 当设置 extensions 中的 vn.invoiceType 时
ObjectNode extensions = JacksonObjectMapper.get().createObjectNode();
VNCommonExtension vnExt = new VNCommonExtension();
vnExt.setInvoiceType("01/MTT");  // 标记为 IndexedExtDataField，映射到 idx_str1
extensions.set("vn", JacksonObjectMapper.get().valueToTree(vnExt));
transaction.setExtensions(extensions);

// 调用框架方法时，自动同步到索引字段
transactionEntityManager.insert(transaction);
// 结果：transaction.idxStr1 = "01/MTT"，同时 ext_data 中存储完整JSON
```

---

## 十、新国家/场景接入扩展字段完整步骤

### 10.1 步骤清单

#### 步骤1：创建国家扩展类（API模块）

**文件**: `document-engine-api/src/main/java/com/baiwang/global/transaction/adaptor/xx/XXCommonExtension.java`

```java
@Setter
@Getter
public class XXCommonExtension {
    
    // 定义ExtensionObjectSpec（用于类型转换）
    public static final ExtensionObjectSpec<XXCommonExtension> KEY = 
        ExtensionObjectSpec.<XXCommonExtension>builder()
            .name("xx")                              // JSON中的key
            .type(XXCommonExtension.class)           // 对应的Java类型
            .defaultValueSupplier(XXCommonExtension::new)
            .build();

    // ===== 带索引的扩展字段（常用查询条件）=====
    @EntityColumnSchema(
        title = "XX Specific Field",
        columnName = "idx_str1",                    // 映射到索引字段
        length = 50,
        columnRole = ColumnRoleType.IndexedExtDataField,
        listShowQuery = true
    )
    private String xxSpecificField;

    @EntityColumnSchema(
        title = "XX Amount",
        columnName = "idx_number1",
        length = 20,
        scale = 2,
        columnRole = ColumnRoleType.IndexedExtDataField
    )
    private BigDecimal xxAmount;

    // ===== 仅JSON存储的扩展字段 =====
    @EntityColumnSchema(
        title = "XX Description",
        columnRole = ColumnRoleType.VirtualExtDataField
    )
    private String xxDescription;
    
    // ===== 复杂类型字段 =====
    @EntityColumnSchema(
        title = "XX Item Codes",
        columnRole = ColumnRoleType.VirtualExtDataField,
        type = ColumnStandardType.EXT_JSON
    )
    private List<String> xxItemCodes;
}
```

#### 步骤2：创建扩展Schema类（Service模块）

**文件**: `document-engine-service/src/main/java/com/baiwang/global/document/adaptor/xx/XXTransactionExtension.java`

```java
@EntityExtensionSchema(
    tableId = "gbw_transaction",
    extNamespace = "xx"
)
@Data
public class XXTransactionExtension extends XXCommonExtension {
}
```

**文件**: `document-engine-service/src/main/java/com/baiwang/global/document/adaptor/xx/XXTransactionLineExtension.java`

```java
@EntityExtensionSchema(
    tableId = "gbw_transaction_line",
    extNamespace = "xx"
)
@Data
public class XXTransactionLineExtension extends XXCommonLineExtension {
}
```

#### 步骤3：实现Schema配置提供者

**文件**: `document-engine-service/src/main/java/com/baiwang/global/document/adaptor/xx/XXTransactionProcessor.java`

```java
@Component
@RequiredArgsConstructor
public class XXTransactionProcessor implements 
        TransactionProcessor, 
        EntityTableSchemaConfigProvider {

    static final Set<TransactionBusinessRoute> ROUTES = Sets.newHashSet(
        TransactionBusinessRoute.builder()
            .countryCode(CountryCode.XX)
            .transactionType(TransactionType.SalesInvoice)
            .build()
    );

    @Override
    public boolean supports(TransactionBusinessRoute route) {
        return ROUTES.contains(route);
    }

    @Override
    public EntityTableSchemaConfigItem getSchemaConfig() {
        EntityTableSchemaConfig xxTransactionSchema = EntitySchemaAnnotationParser.parseExtension(
            XXTransactionExtension.class,
            XXTransactionLineExtension.class
        );
        return EntityTableSchemaConfigItem.builder()
            .tableSchemaID(EntityTableSchemaID.parse("global.XX"))
            .tableSchemaConfig(xxTransactionSchema)
            .build();
    }

    @Override
    public void beforeCreateTransaction(TransactionCreateContext context) {
        Transaction transaction = context.getTransaction();
        // 处理XX国家特定的逻辑
    }
}
```

#### 步骤4：扩展字段使用

**设置扩展字段**:
```java
// 创建ObjectNode
ObjectNode extensions = JacksonObjectMapper.get().createObjectNode();

// 创建XX扩展对象
XXCommonExtension xxExtension = new XXCommonExtension();
xxExtension.setXxSpecificField("value");
xxExtension.setXxAmount(new BigDecimal("100.00"));
xxExtension.setXxItemCodes(Arrays.asList("ITEM001", "ITEM002"));

// 转换为JsonNode并设置
String jsonString = JacksonObjectMapper.get().toJsonString(xxExtension);
JsonNode xxNode = JacksonObjectMapper.get().toJsonNode(jsonString);
extensions.set("xx", xxNode);

// 设置到DTO
transactionDTO.setExtensions(extensions);
```

**读取扩展字段**:
```java
// 从Entity读取
XXCommonExtension xxExtension = ExtensionUtils.getExtension(
    "xx", 
    XXCommonExtension.class, 
    transaction.getExtensions()
);

if (xxExtension != null) {
    String specificField = xxExtension.getXxSpecificField();
    BigDecimal amount = xxExtension.getXxAmount();
    List<String> itemCodes = xxExtension.getXxItemCodes();
}
```

### 10.2 注意事项

1. **索引字段数量有限**: 
   - idx_str: 15个
   - idx_date: 5个
   - idx_number: 10个
   合理规划，仅将常用作查询条件的字段映射到索引

2. **JSON大小限制**: ext_data 存储在MySQL JSON字段中，单个扩展对象不宜过大（建议不超过1MB）

3. **版本兼容性**: 扩展字段的修改要考虑历史数据的兼容性，建议：
   - 新增字段设置默认值
   - 避免删除已使用的字段
   - 字段重命名时保持向后兼容

4. **索引字段同步**: 框架会自动同步 `IndexedExtDataField` 到索引字段，无需应用层手动处理

---

## 十一、关键文件清单

| 文件 | 路径 | 说明 |
|-----|------|------|
| Transaction.java | `document-engine-service/.../dao/entity/Transaction.java` | 主表实体类 |
| TransactionExt.java | `document-engine-service/.../dao/entity/TransactionExt.java` | 扩展表实体类 |
| TransactionLine.java | `document-engine-service/.../dao/entity/TransactionLine.java` | 明细行实体类 |
| TransactionLineExt.java | `document-engine-service/.../dao/entity/TransactionLineExt.java` | 明细行扩展表实体类 |
| TransactionDTO.java | `document-engine-api/.../model/dto/TransactionDTO.java` | DTO定义 |
| TransactionDAO.java | `document-engine-service/.../dao/TransactionDAO.java` | DAO实现 |
| TransactionConverter.java | `document-engine-service/.../converter/TransactionConverter.java` | 转换器 |
| ExtensionUtils.java | `document-engine-api/.../utils/ExtensionUtils.java` | 扩展字段工具类 |
| VNCommonExtension.java | `document-engine-api/.../adaptor/vn/VNCommonExtension.java` | 越南扩展字段定义 |
| VNTransactionExtension.java | `document-engine-service/.../adaptor/vn/VNTransactionExtension.java` | 越南扩展Schema |
| VNTransactionProcessor.java | `document-engine-service/.../adaptor/vn/VNTransactionProcessor.java` | 越南处理器+Schema提供者 |
| DocumentEngineConfiguration.java | `document-engine-service/.../config/DocumentEngineConfiguration.java` | Schema管理器配置 |


path: evals/evals.json
content: {
  "skill_name": "xbridge-ext-helper",
  "evals": [
    {
      "id": 1,
      "prompt": "我需要在 xbridge3 框架中为泰国（TH）业务添加扩展字段，包括发票类型（映射到 idx_str1）、折扣金额（映射到 idx_number1）和支付方式（仅JSON存储）。请帮我生成完整的代码，包括 CommonExtension 类、Schema 配置类和 Processor 类。",
      "expected_output": "完整的泰国扩展字段实现代码，包括：THCommonExtension.java（带 IndexedExtDataField 和 VirtualExtDataField 注解）、THTransactionExtension.java（带 @EntityExtensionSchema）、THTransactionProcessor.java（实现 EntityTableSchemaConfigProvider）",
      "files": []
    },
    {
      "id": 2,
      "prompt": "如何在 Validator 中读取越南（VN）扩展字段的发票类型并进行校验？请给我代码示例。",
      "expected_output": "使用 ExtensionUtils.getExtension() 读取越南扩展字段，并校验 invoiceType 字段的完整代码示例",
      "files": []
    },
    {
      "id": 3,
      "prompt": "我在 DAO 层创建交易时，扩展字段保存到 gbw_transaction 表了，但是 gbw_transaction_ext 表没有数据，怎么回事？",
      "expected_output": "解释框架不会自动处理扩展表插入，需要手动调用 transactionExtMapper.insert() 的代码示例",
      "files": []
    },
    {
      "id": 4,
      "prompt": "请解释 IndexedExtDataField 和 VirtualExtDataField 的区别，以及 idx_str/idx_number 索引字段的分配建议。",
      "expected_output": "清晰解释两种角色类型的区别（存储位置、查询方式）、索引字段配额（15个str、5个date、10个number）、分配建议",
      "files": []
    },
    {
      "id": 5,
      "prompt": "我需要在 Converter 中处理 Transaction 和 TransactionDTO 之间的扩展字段转换，应该怎么做？",
      "expected_output": "说明扩展字段应该直接透传（setExtensions(source.getExtensions())），不要逐个字段转换的代码示例",
      "files": []
    }
  ]
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)