PAYATHON 2026

如何为不同格式的银行对账单设计可维护的交易分类 fallback pipeline?

技术老齐

chain/fallback 适合这个设计:先用明确的规则分类,再逐步加入商户匹配和 ML。现有接口和串联方式足以支撑第一版,暂时不需要引入规则引擎。

先确定分类在什么条件下可以结束,以及多个来源冲突时采用哪个结果。类里有多少 if/else,可以放在后面考虑。

1. 以成功分类作为停止条件

银行类别有值,也可能无法映射到内部类别。例如,银行返回 Other 或尚未配置的类别时,都应继续尝试 MCC、商户别名等分类器。

接口契约可以统一为:

  • 字段缺失、映射不存在或证据不足时,返回 Optional.empty()。
  • 得到可接受的分类结果时,才返回非空结果。
  • 所有分类器都未能分类时,由 pipeline 生成 UNKNOWN。

如果某个分类器返回 Optional.of(UNKNOWN),当前的 findFirst() 就会提前停止,后续分类器无法运行。

“无法判断”和“执行失败”也要分开处理。找不到映射属于正常回退;规则加载失败、模型超时属于运行故障,需要单独记录,并明确是否允许降级。

2. 按业务策略安排顺序

“第一个命中即返回”意味着前面的结果优先于后面的结果。将银行类别放在 MCC 之前,需要有业务依据。

可以先采用以下顺序,再根据人工核验的数据调整:

  1. 当前交易的人工修正结果。
  2. 范围明确的用户规则,以及可靠的交易类型规则。
  3. 已验证可用的银行类别映射。
  4. MCC 映射。
  5. 商户别名精确匹配。
  6. 描述关键词规则。
  7. 达到接受条件的 ML 结果。
  8. UNKNOWN。

交易类型规则可以使用 transactionTypeCode。如果某家银行的代码能明确识别自有账户转账,就可以先于消费类别处理。代码含义要按银行解释,不能假设各银行通用。

商户匹配和 MCC 的顺序也可以调整。经过核验、适用范围明确的商户规则,可能比宽泛的 MCC 分类更准确。

题目中的示例可以这样处理:

交易可以采用的判断路径
AH TO GO按示例中的 MCC 映射得到 GROCERIES
NS REIZIGERS按该银行的 Transport 映射得到 TRANSPORT
Netflix商户别名命中 ENTERTAINMENT
John Smith根据交易类型或描述判断转账,姓名本身不提供分类依据

Transfer to own account 可以作为关键词规则的依据,但仅凭描述无法验证账户归属。如果业务需要严格区分内部转账,还需要账户标识等信息。

在 Spring 中,应显式配置分类器顺序,例如在配置类中按顺序组装列表。使用 @Order 时,也应保留验证完整顺序的测试,避免新增 Bean 后意外改变分类结果。

3. 将标准化放在分类链之前

标准化是共享的预处理步骤,在进入 pipeline 前执行一次即可。如果商户分类器和关键词分类器各自清洗数据,最终可能使用不同的文本。

建议同时保留:

  • 原始字段,用于排查问题和重新处理。
  • 标准化字段,用于规则匹配和模型输入。

大小写、空白和约定的标点形式可以统一处理。银行特有的付款前缀、卡号尾号等,则按来源格式处理。不要直接删除所有数字或地点文本,其中也可能包含有效的商户信息。

分类上下文中还需要补充 bankId。同一个类别名称或交易类型代码,在不同银行可能含义不同。银行标识可以放在独立的分类上下文中,有需要时再附带解析器版本、账户标识和币种。

商户名称与交易描述也应分开保留。将两者拼成一个字符串,会模糊商户精确匹配和描述关键词匹配的边界。

4. 让结果包含分类依据

Optional<TransactionCategory> 可以满足分类需求。为了在误判时查到具体规则,可以扩展返回结果,保留现有 chain 结构:

public record ClassificationInput(
    ParsedTransaction transaction,
    String bankId,
    String normalizedMerchant,
    String normalizedDescription
) {}

public record ClassificationMatch(
    TransactionCategory category,
    String source,
    String ruleId,
    String version
) {}

public interface TransactionClassifier {
    Optional<ClassificationMatch> classify(ClassificationInput input);
}

非空结果必须通过接受条件,且类别不能是 UNKNOWN。串联逻辑可以写成:

public final class TransactionClassificationService {

    private final List<TransactionClassifier> classifiers;

    public TransactionClassificationService(
        List<TransactionClassifier> classifiers
    ) {
        this.classifiers = List.copyOf(classifiers);
    }

    public ClassificationMatch classify(ClassificationInput input) {
        for (TransactionClassifier classifier : classifiers) {
            Optional<ClassificationMatch> match =
                classifier.classify(input);

            if (match.isPresent()) {
                return match.get();
            }
        }

        return new ClassificationMatch(
            TransactionCategory.UNKNOWN,
            "FALLBACK",
            null,
            null
        );
    }
}

循环和原来的 Stream 写法都可以。循环更方便设置断点,后续加入耗时统计或受控降级也更顺手。

version 应标识所用规则集或模型的版本。如果需要重现历史结果,还应记录 pipeline 和标准化逻辑的版本。

5. 根据维护方式选择映射数据的存储位置

分类器负责匹配算法,通过独立的数据访问接口获取映射数据。这样从配置文件迁移到数据库时,就不需要修改分类流程。

维护方式合适的存储位置
规则较少,由开发者维护,随应用发布Git 管理的 YAML、JSON 或 CSV
需要后台编辑、即时更新、用户独立配置数据库
系统提供默认映射,允许用户覆盖默认配置加数据库覆盖规则

第一版可以使用纳入版本控制的配置文件,便于审查和回滚。等明确需要在线编辑时,再引入数据库和管理界面。

无论存储在哪里,都要明确规则的适用范围:

数据建议包含的信息
银行类别映射银行标识、原始类别、目标类别、启用状态
MCC 映射MCC 字符串、目标类别、版本
商户别名标准化别名、商户标识、适用范围
关键词规则匹配字段、匹配方式、模式、目标类别、优先级

MCC 继续用字符串保存,避免丢失前导零。

商户别名最好先映射到商户标识,再由商户规则决定类别。例如,AH 和 Albert Heijn 是否属于同一商户,可以单独维护;AH TO GO 是否采用相同分类,则由分类口径决定,不必在处理别名时就强行合并。

短别名应优先使用精确匹配或有边界的匹配,直接使用 description.contains("AH") 很容易误判。关键词规则也要规定同一阶段多条规则命中时如何处理,例如先采用更具体的规则,再按显式优先级处理;无法消除歧义时,可以放弃判断。

数据库中的规则不必每笔交易都查一次。可以加载经过校验的规则快照供分类器使用,更新时整体切换版本,避免同一批导入混用新旧规则。

6. ML 可以沿用同一个接口

后续加入 MachineLearningClassifier 时,继续遵守相同契约:

  • 接收统一的分类上下文。
  • 模型输出通过接受条件后,返回分类结果。
  • 证据不足时返回 Optional.empty()。

初期可以把 ML 放在规则之后,只处理剩余交易。正式接入前,先让它并行生成预测,暂不影响最终分类,再用人工核验数据评估表现。

模型分数不应直接与人工规则的“置信度”比较,两者分别表达预测概率和规则优先级。模型的接受阈值应根据验证数据确定,必要时按类别设置。

如果未来需要同时比较银行类别、MCC 和模型给出的多个候选结果,就应将接口扩展为生成候选结果,再由单独的决策器解决冲突。当前需求下,采用首个有效结果即可。

7. 初期就要处理的维护问题

人工修正的作用范围

用户修改一笔交易,只能直接确定这笔交易的类别。同一商户可能对应不同用途,因此这次修改不应自动变成全局商户规则。

自动分类结果、人工修正结果,以及用户明确创建的规则,应分别保存。重新分类时,仍要优先采用人工修正结果。

类别口径与交易方向

先明确退款、收入、自有账户转账、信用卡还款等情况如何表示。TRANSFER 描述资金流动类型,GROCERIES 描述消费用途。如果后续报表需要同时表达两者,可以拆分为不同字段。

金额正负的含义也应在银行解析层统一,避免不同银行的借贷记账方式影响分类。

回归数据与评估

按银行保留经过核验的样例,覆盖字段缺失、未知类别、噪声描述、规则冲突和多条规则同时命中的情况。每次修改规则,都要检查已有分类是否发生非预期变化。

评估时,除了 UNKNOWN 占比,还要看各分类器的命中量和抽样错误率。未知交易减少,不一定代表分类更准确。

训练 ML 时,应区分人工确认标签和系统自动标签。划分训练集与测试集时,还要防止重复导入的记录落入两边,导致评估结果过于乐观。

历史结果与敏感信息

规则更新后,应明确历史交易是保留原结果,还是允许批量重新分类,并保留结果版本。调试记录尽量使用交易标识、规则编号和脱敏信息,避免在普通日志中复制完整账单描述或账户数据。

备注:内容仅供参考。