跳到正文
Kale的数字花园
目录 文章
/22 分钟/10 阅读/0 评论

多平台财务核算系统

集团旗下跨境电商业务覆盖 eBay、AliExpress(速卖通)、Wish、Shopee、Walmart、Allegro、美客多、Joom、Joybuy、CDiscount、自建站(Shopify)等 **13+ 电商平台**,每个平台的

1. 背景与痛点

业务背景

集团旗下跨境电商业务覆盖 eBay、AliExpress(速卖通)、Wish、Shopee、Walmart、Allegro、美客多、Joom、Joybuy、CDiscount、自建站(Shopify)等 13+ 电商平台,每个平台的财务结算规则、数据格式、币种体系、费用结构各不相同。财务部门每月需要对所有平台的订单收入、发款/结算、PayPal/支付宝流水、广告费、保险理赔、仓储费等数据进行月度核算,最终输出各平台的客户账单和检查报表。

技术痛点

  1. 数据异构严重:13+ 平台的财务数据格式完全不同(CSV/Excel/API),字段命名、币种、日期格式、费用分类各异,人工处理极易出错
  2. 数据量大、处理耗时:单平台单月订单数据可达数十万条,发款/结算/PayPal 等多维数据交叉匹配,手工 Excel 处理一个平台就要 2-3 天
  3. 匹配逻辑复杂:订单需要匹配客户、品类、销售归属,匹配维度包括平台 SKU、Item Number、订单号、自定义标签等,且存在多级回退匹配策略
  4. 对账差异难追踪:发款与订单的金额差异、历史调整冲减、多币种汇率转换等,手工对账几乎无法保证准确性
  5. 关账管控缺失:缺乏系统化的关账机制,数据可能在核算完成后被误改,导致报表数据不一致

2. 技术方案

整体架构

采用 Spring Boot + MyBatis Plus + 多数据源 架构,每个平台独立数据库,通过统一的任务驱动框架串联整个核算流程。

┌─────────────────────────────────────────────────────────────┐
│                    前端(Vue.js + Element UI)                │
├─────────────────────────────────────────────────────────────┤
│                    Controller 层(REST API)                  │
├─────────────────────────────────────────────────────────────┤
│                    Service 层(业务逻辑)                      │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │
│  │ 数据导入  │ │ 数据清洗  │ │ 数据匹配  │ │  对账计算 │       │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐                    │
│  │ 财务调整  │ │ 报表导出  │ │  关账管理 │                    │
│  └──────────┘ └──────────┘ └──────────┘                    │
├─────────────────────────────────────────────────────────────┤
│              GeneralTask 异步任务驱动框架                      │
│  ┌──────────────────────────────────────────────────┐       │
│  │ IGeneralTaskHandler(策略模式 + 异步执行)          │       │
│  │ → 任务创建 → 互斥检查 → 关账校验 → 异步执行 → 进度更新 │   │
│  └──────────────────────────────────────────────────┘       │
├─────────────────────────────────────────────────────────────┤
│              Mapper 层(MyBatis Plus + 复杂 SQL)             │
├──────┬──────┬──────┬──────┬──────┬──────┬──────┬───────────┤
│eBay库│SMT库 │Wish库│Shopee│Walmart│Allegro│Joom │ ...13+库  │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴───────────┘

核心流程(月度核算 Pipeline)

① 数据导入 → ② 数据清洗 → ③ 数据匹配 → ④ 对账计算 → ⑤ 财务调整 → ⑥ 报表输出 → ⑦ 关账

每个步骤均封装为 GeneralTask,通过 IGeneralTaskHandler 策略模式分发到对应平台的处理器,支持异步执行、进度追踪、失败重试。

多数据源隔离策略

每个平台独立数据库,通过 Flyway 管理 258+ 个迁移脚本:

数据库 用途 迁移脚本数
codo_finance_base 基础库(汇率、关账、任务) 19
codo_finance_ebay eBay 核算库 48
codo_finance_smt 速卖通核算库 49
codo_finance_walmart Walmart 核算库 25
codo_finance_shopee Shopee 核算库 23
codo_finance_allegro Allegro 核算库 18
... 其余 7+ 平台 ...
codo_pms SKU 对照表库 4

3. 实现细节

3.1 数据导入

财务人员通过前端上传各平台导出的 Excel/CSV 文件,系统自动完成解析入库:

// GeneralTaskServiceImpl - 创建导入任务
public GeneralTask createImportTask(String taskType, MultipartFile file) {
    // 1. 关账校验 —— 已关账月份禁止导入
    checkFinanceClose(actionDate, platform);
    // 2. 任务互斥检查 —— 同类型任务不允许并发
    checkTaskMutex(taskType);
    // 3. 文件存储 + 编码检测
    String filePath = financeFileStorage.store(file);
    EncodingDetect.detect(filePath);  // 自动识别 UTF-8/GBK
    // 4. 创建任务记录,异步启动处理
    GeneralTask task = buildTask(taskType, filePath);
    startHandleTask(task);  // @Async 异步执行
    return task;
}

文件解析支持多种格式:

  • Excel:使用 POI 4.0 + FastExcel 流式读取,避免大文件 OOM
  • CSV:自动编码检测(EncodingDetect),支持 UTF-8/GBK/GB2312
  • ZIP:Zip4j 解压后逐文件处理

3.2 数据清洗(以 eBay 为例)

清洗是核心环节,EbayDetailCleanServiceImpl 包含 7 个清洗方法,以订单清洗为例:

public void orderClean(GeneralTask task) {
    // Step 1: 去重 —— 删除与历史月份重复的订单
    ebayDetailCleanMapper.deleteWithHistoryDuplicateOrder(fileId);
    // Step 2: 删除本月内部重复
    ebayDetailCleanMapper.deleteWithThisMonthDuplicateOrder(fileId);
    // Step 3: 店铺名称标准化(统一大写)
    ebayDetailCleanMapper.orderShopToUpperCase(fileId);
    // Step 4: 清洗自定义标签中的 & 转义符
    ebayDetailCleanMapper.cleanOrderCustomerLabelAmp(fileId);
    // Step 5: 根据店铺获取订单组别
    ebayDetailCleanMapper.obtainOrderGroup(fileId);
    // Step 6: 清洗运费中的币种符号(如 "$12.5" → "12.5")
    ebayDetailCleanMapper.cleanShippingPriceSymbol(fileId);
    // Step 7: 根据 Listing 获取站点
    ebayDetailCleanMapper.obtainOrderSiteWithListing(fileId);
    // Step 8: 根据币种推断站点(Listing 匹配不到时的回退)
    ebayDetailCleanMapper.obtainOrderSiteWithCurrency(fileId);
    // Step 9: 计算订单收入 = Sale Price × Quantity + Shipping
    ebayDetailCleanMapper.calNMCOAndChildOrderexcludeRefundIncome(fileId);
}

清洗逻辑的关键设计:

  • 去重策略:先跨月去重(与历史数据比对),再月内去重,确保不重复核算
  • 多级回退:站点获取先走 Listing 匹配,匹配不到再走币种推断
  • 批量 SQL:所有清洗操作直接在数据库层面用 UPDATE/DELETE 完成,避免数据搬运到应用层

3.3 数据匹配(多级回退匹配策略)

匹配是整个系统最复杂的环节。以 eBay 发款匹配客户为例,EbayDetailMatchServiceImpl 实现了 5 级回退匹配:

匹配优先级(从高到低):
┌─────────────────────────────────────────────────┐
│ Level 1: 自定义标签 → SKU对照表 → 客户/品类      │
│   ↓ 匹配失败                                     │
│ Level 2: 订单号 + Item Number → 近一年订单数据    │
│   ↓ 匹配失败                                     │
│ Level 3: 仅 Item Number → SKU对照表              │
│   ↓ 匹配失败                                     │
│ Level 4: 仅订单号 → 订单数据                      │
│   ↓ 匹配失败                                     │
│ Level 5: 特殊规则处理(客户名称合并等)            │
└─────────────────────────────────────────────────┘
// EbayDetailMatchServiceImpl - 发款匹配客户核心逻辑
public void paymentMatchCustomer(GeneralTask task) {
    // Level 1: 自定义标签匹配
    ebayDetailMatchMapper.paymentMatchCustomerByCustomLabel(fileId);
    
    // Level 2: 订单号 + Item Number 联合匹配(查近一年订单)
    ebayDetailMatchMapper.paymentMatchCustomerByOrderAndItem(fileId);
    
    // Level 3: 仅 Item Number 匹配 SKU 对照表
    ebayDetailMatchMapper.paymentMatchCustomerByItemNumber(fileId);
    
    // Level 4: 仅订单号匹配
    ebayDetailMatchMapper.paymentMatchCustomerByOrderNumber(fileId);
    
    // Level 5: 特殊规则 —— 客户名称合并(如"新捷丽A/B"→"新捷丽")
    ebayDetailMatchMapper.paymentMergeCustomerName(fileId);
    
    // 旧客户品类用仓库 SKU 匹配产品 SKU 对照表
    ebayDetailMatchMapper.paymentMatchCategoryByWarehouseSku(fileId);
}

匹配设计的核心思路:宽进严出,逐级降级。优先用最精确的维度匹配,匹配不到再放宽条件,最大化匹配覆盖率的同时保证准确性。

3.4 对账计算(临时表 Pipeline)

对账是将发款/结算数据与订单数据进行金额核对,找出差异。EbayReconcileServiceImpl 采用临时表流水线模式:

// 发款关联订单 —— 核心对账流程
public void paymentRelateOrder(GeneralTask task, String actionDate) {
    // Step 1: 清空临时表(每次对账重新计算)
    ebayReconcileMapper.truncateTempTables();
    
    // Step 2: 将结算数据插入临时结算表
    ebayReconcileMapper.insertTempSettlement(actionDate);
    
    // Step 3: 插入临时结算订单表(结算 JOIN 订单)
    ebayReconcileMapper.insertTempSettlementOrder(actionDate);
    
    // Step 4: 插入临时结算订单单笔表
    ebayReconcileMapper.insertTempSettlementOrderSingle(actionDate);
    
    // Step 5: 按月汇总
    ebayReconcileMapper.insertTempSettlementOrderSingleMonthly(actionDate);
    
    // Step 6: 计算关联订单差异金额
    ebayReconcileMapper.insertTempSettlementRelateOrderCalc(actionDate);
    
    // Step 7: 修正计算结果
    ebayReconcileMapper.updateTempSettlementRelateOrderCalc(actionDate);
    
    // Step 8: 写入正式对账结果表
    ebayReconcileMapper.insertSettlementRelateOrder(actionDate);
    
    // Step 9: 自动冲减 —— 本月结算了历史已调整的订单,自动插入冲减记录
    ebayReconcileMapper.autoInsertAdjustReverse(actionDate);
}

临时表 Pipeline 的设计优势:

  • 中间结果可审计:每一步的临时表都可以查看,方便排查对账差异
  • 计算性能高:复杂的多表 JOIN 和聚合在数据库层面完成,避免应用层大量数据搬运
  • 幂等性:每次对账先 truncate 临时表,重跑不会产生脏数据

3.5 关账管控

// FinanceCloseRecordServiceImpl - 关账逻辑
public void closeOrCancelActionDateFinance(String actionDate, String platform, Integer type) {
    if (type == 1) {
        // 关账:插入关账记录,后续所有导入/清洗/匹配/对账操作都会校验
        financeCloseRecordMapper.insert(buildCloseRecord(actionDate, platform));
    } else {
        // 取消关账:逻辑删除
        financeCloseRecordMapper.logicDelete(actionDate, platform);
    }
}

// 所有任务创建前的关账校验
private void checkFinanceClose(String actionDate, String platform) {
    if (financeCloseRecordService.haveFinanceCloseRecord(actionDate, platform)) {
        throw new BusinessException("该月份已关账,禁止操作");
    }
}

3.6 异步任务框架(GeneralTask + 策略模式)

// IGeneralTaskHandler —— 策略接口
public interface IGeneralTaskHandler {
    String getTaskType();           // 任务类型标识
    void handler(GeneralTask task); // 执行逻辑
}

// 任务分发 —— 根据 taskType 找到对应 Handler
@Service
public class GeneralTaskServiceImpl {
    @Autowired
    private List<IGeneralTaskHandler> handlers; // Spring 自动注入所有实现类
    
    @Async  // 异步执行
    public void startHandleTask(GeneralTask task) {
        IGeneralTaskHandler handler = handlers.stream()
            .filter(h -> h.getTaskType().equals(task.getTaskType()))
            .findFirst().orElseThrow();
        try {
            handler.handler(task);
            markTaskCompleted(task);
        } catch (Exception e) {
            markTaskFailed(task, e.getMessage());
        }
    }
}

系统定义了 100+ 种任务类型(GeneralTaskTypeNameEnum),涵盖导入、清洗、匹配、对账、导出等全流程。


4. 技术栈与亮点归纳

技术栈清单

分类 技术
后端框架 Spring Boot 2.0.4、MyBatis Plus 3.4.1
数据库 MySQL(14 个独立库)、Druid 1.1.9 连接池
缓存 Redis(Redisson 3.13.3)
数据库版本管理 Flyway(258+ 迁移脚本)
Excel 处理 Apache POI 4.0.1、FastExcel 0.15.3(流式读取)
文件处理 Zip4j 1.3.1、Commons IO 2.6
权限管理 Apache Shiro 1.4.0
工具库 Hutool 5.6.6、Lombok、Joda Time 2.9.9
前端 Vue.js + Element UI + Webpack
线程池 自定义线程池(核心 50、最大 100)

技术亮点

  • 多数据源隔离:14 个平台独立数据库,Flyway 统一管理 258+ 迁移脚本,平台间数据完全隔离
  • 策略模式驱动的任务框架:IGeneralTaskHandler + 100+ 任务类型,新增平台只需实现 Handler
  • 多级回退匹配算法:5 级匹配策略,从精确到模糊逐级降级,最大化匹配覆盖率
  • 临时表 Pipeline 对账:多步临时表流水线,中间结果可审计,支持幂等重跑
  • 流式 Excel 处理:FastExcel + XSSFReaderExtends 流式读取,处理大文件不 OOM
  • 批量操作封装:BatchOps 工具类,默认 500 条/批,避免单次 SQL 过大
  • 关账管控机制:平台级关账,所有写操作前置校验,防止已核算数据被篡改

5. 难点与亮点剖析

业务难点

  1. 13+ 平台数据异构:每个平台的结算规则、费用结构、数据格式完全不同,eBay 有 Transaction Report + PayPal + Invoice 三套数据,AliExpress 有结算单 + 支付宝 + 广告费 + 保险理赔四套数据,Walmart 还有运费和仓储费独立核算
  2. 匹配维度多且模糊:同一笔订单在不同数据源中的标识不一致(订单号、Item Number、SKU、自定义标签),需要多维度交叉匹配
  3. 历史数据关联:本月结算可能包含历史月份的订单,对账时需要跨月回溯,且历史已调整的订单需要自动冲减
  4. 多币种核算:涉及 USD、EUR、GBP、CNY 等多种币种,需要按核算月汇率统一转换
  5. 财务准确性要求极高:一分钱的差异都需要追踪到具体原因,不允许"大差不差"

技术难点

  1. 大数据量批处理性能:单平台单月数据量数十万条,多表 JOIN 对账计算耗时长
  2. 复杂 SQL 维护成本:对账涉及 8+ 步临时表操作,单个 Mapper XML 可达数百行
  3. 多数据源事务管理:14 个数据库的连接池管理、事务隔离
  4. 任务并发控制:同类型任务不能并发执行(如同时清洗同一批数据),需要互斥机制

实现亮点

  1. 策略模式 + Spring 自动注入:新增平台只需实现 IGeneralTaskHandler,无需修改调度逻辑,完美符合开闭原则
  2. 临时表 Pipeline:将复杂对账拆解为多步可审计的中间步骤,既保证性能又方便排错
  3. 多级回退匹配:5 级匹配策略将匹配覆盖率从最初的 ~70% 提升到 95%+
  4. 关账前置校验:所有写操作统一拦截,从架构层面杜绝已核算数据被误改

6. 替代方案对比

方案一:纯 Excel 手工核算(原始方案)

维度 手工 Excel 当前系统
处理效率 单平台 2-3 天 单平台 10-30 分钟
准确率 ~90%(人工失误) 99%+(系统校验)
可追溯性 差(Excel 版本混乱) 好(任务记录 + 临时表审计)
扩展性 新平台需重新建模板 实现 Handler 即可
成本 财务人员 3-5 人 1 人操作 + 系统自动化

结论:手工方案在平台数量少(1-2 个)时可行,但 13+ 平台规模下完全不可持续。

方案二:统一数据库 + 平台标识字段

将所有平台数据放在同一个数据库,用 platform 字段区分。

维度 统一库 多库隔离(当前)
运维复杂度 低(单库) 高(14 个库)
数据隔离 弱(靠字段区分) 强(物理隔离)
表结构灵活性 差(需兼容所有平台) 好(每个平台独立设计)
查询性能 差(数据量大、索引复杂) 好(单库数据量可控)
迁移风险 高(一个平台出问题影响全部) 低(平台间互不影响)

结论:统一库方案在平台少、数据结构相似时更简单,但 13+ 平台且数据结构差异巨大的场景下,多库隔离是更稳妥的选择。各平台的结算规则、费用结构差异太大,强行统一表结构会导致大量冗余字段和复杂的条件判断。


7. 技术选型与方案抉择复盘

选型思路

  • 团队栈:团队以 Java + Spring Boot 为主,选择 MyBatis Plus 降低 SQL 编写成本
  • 时间约束:需要快速支持新平台接入,策略模式 + 模板化设计是最优解
  • 数据规模:单平台月数据量在十万级,MySQL 完全能扛住,无需引入大数据组件
  • 运维能力:团队运维能力有限,多库方案虽然库多但每个库都很简单,比复杂的分表方案更易维护

优点

  1. 平台扩展成本低:新增平台只需实现对应的 Clean/Match/Reconcile Service + Handler,核心框架无需改动
  2. 数据隔离彻底:平台间完全独立,一个平台的 schema 变更不影响其他平台
  3. Flyway 版本管理:258+ 迁移脚本确保数据库变更可追溯、可回滚
  4. 任务可观测:每个操作都有 GeneralTask 记录,进度、状态、耗时一目了然

缺点 / 已知不足

  1. 多库运维负担:14 个数据库的备份、监控、连接池管理成本较高
  2. 代码重复度:各平台的 Clean/Match/Reconcile 逻辑有 60-70% 相似,但因为业务差异无法完全抽象
  3. 缺乏实时性:月度批处理模式,无法支持实时对账需求
  4. 前端体验一般:Vue.js + Element UI 的管理后台,交互体验偏传统

可优化点

  1. 抽象公共清洗/匹配模板:将各平台 60-70% 的公共逻辑抽取为模板方法,平台差异部分用钩子方法实现
  2. 引入消息队列:用 Kafka 解耦导入和处理流程,支持更大数据量的异步处理
  3. 数据库连接池优化:14 个库可以考虑按使用频率动态调整连接池大小
  4. 增加数据质量监控:在清洗和匹配环节增加数据质量指标(匹配率、异常率)的自动告警

8. 成果与数据

指标 数据
覆盖平台数 13+ 个电商平台(eBay、AliExpress、Wish、Shopee、Walmart 等)
数据库规模 14 个独立数据库,258+ Flyway 迁移脚本
任务类型 100+ 种(导入/清洗/匹配/对账/导出)
月度数据处理量 单平台数十万条,全平台百万级
核算效率提升 单平台从 2-3 天缩短到 10-30 分钟,整体效率提升 10 倍+
匹配覆盖率 多级回退匹配策略将覆盖率从 ~70% 提升到 95%+
对账准确率 系统化对账准确率 99%+,差异可追溯到具体订单
人力节省 财务核算人员从 3-5 人减少到 1 人操作
线程池配置 核心线程 50、最大线程 100,支持多任务并行处理

9. 面试题清单

1. 为什么选择多数据源隔离而不是单库多表?

  • 考察点:数据库架构设计、多数据源管理、数据隔离策略
  • 问题剖析:这个问题的核心是理解"什么时候该拆库"。常见误区是认为拆库一定比单库好,或者反过来认为单库一定更简单。关键要从数据结构差异度、隔离需求、运维成本三个维度分析。
  • 思考过程:先说业务场景(13+ 平台数据结构差异大),再说技术考量(隔离性、灵活性、性能),最后说权衡(运维成本 vs 收益)。
  • 详细回答:
    ① 业务驱动:13+ 电商平台的财务数据结构差异巨大,eBay 有 Transaction Report + PayPal + Invoice 三套数据,AliExpress 有结算单 + 支付宝 + 广告费四套,强行统一表结构会导致大量冗余字段和复杂的条件判断
    ② 隔离性:多库方案下,一个平台的 schema 变更、数据量增长、慢查询都不会影响其他平台,故障隔离彻底
    ③ 灵活性:每个平台可以独立设计表结构,字段命名贴合平台术语,开发和维护更直观
    ④ 性能:单库数据量可控(单平台月数据十万级),索引设计更简单,查询性能有保障
    ⑤ 权衡:多库的运维成本确实更高(14 个库的备份、监控),但通过 Flyway 统一管理迁移脚本(258+ 个),以及 Druid 连接池的统一配置,运维负担可控
    ⑥ 如果平台数量少(3 个以内)且数据结构相似,单库 + 平台标识字段是更简单的选择

2. 多级回退匹配策略是怎么设计的?匹配率怎么保证?

  • 考察点:算法设计、数据匹配策略、业务理解深度
  • 问题剖析:匹配问题的核心难点是"数据标识不一致"——同一笔交易在不同数据源中的标识字段不同。常见误区是只用单一维度匹配,导致覆盖率低;或者匹配条件太宽松,导致误匹配。
  • 思考过程:先解释为什么需要多级匹配(数据标识不一致),再说每一级的匹配逻辑和优先级,最后说效果和兜底策略。
  • 详细回答:
    ① 问题根源:同一笔订单在订单表、发款表、PayPal 表中的标识字段不完全一致,有的有 Item Number,有的只有订单号,有的只有自定义标签
    ② 5 级匹配策略:Level 1 用自定义标签匹配 SKU 对照表(最精确)→ Level 2 用订单号 + Item Number 联合匹配近一年订单 → Level 3 仅用 Item Number 匹配 SKU 对照表 → Level 4 仅用订单号匹配 → Level 5 特殊规则处理(客户名称合并等)
    ③ 设计原则是"宽进严出,逐级降级":优先用最精确的维度,匹配不到再放宽条件
    ④ 效果:多级策略将匹配覆盖率从最初的 ~70% 提升到 95%+,剩余 5% 需要人工确认
    ⑤ 防误匹配:每一级匹配都有明确的前置条件(如"客户字段为空且 Item Number 不为空"才进入下一级),避免已匹配的数据被覆盖

3. 临时表 Pipeline 对账方案的优劣是什么?为什么不用应用层计算?

  • 考察点:数据库设计、批处理架构、性能优化思路
  • 问题剖析:对账计算有两种主流方案:数据库层面(临时表 + 复杂 SQL)和应用层面(Java 代码计算)。核心权衡是性能 vs 可维护性。常见误区是一味追求"代码可读性"而忽略数据搬运成本。
  • 思考过程:先说方案是什么(8 步临时表流水线),再说为什么选这个方案(性能、审计),最后坦诚说缺点(SQL 维护成本高)。
  • 详细回答:
    ① 方案描述:对账流程拆解为 8 步临时表操作,每一步将中间结果写入临时表,下一步基于上一步的临时表继续计算,最终写入正式结果表
    ② 选择原因——性能:对账涉及订单表、发款表、结算表等多表 JOIN,数据量在十万级,如果把数据拉到应用层再用 Java 计算,网络传输 + 内存占用 + 循环匹配的开销远大于数据库层面的 JOIN 操作
    ③ 选择原因——可审计:每一步的临时表都可以直接查看,财务人员发现对账差异时,可以逐步排查是哪一步出了问题
    ④ 选择原因——幂等性:每次对账先 truncate 临时表,重跑不会产生脏数据
    ⑤ 缺点:复杂 SQL 的可读性和维护成本高,单个 Mapper XML 可达数百行;调试困难,需要逐步检查临时表数据
    ⑥ 适用场景:数据量在十万到百万级、计算逻辑以 JOIN 和聚合为主的批处理场景。如果是实时计算或逻辑分支复杂的场景,应用层计算更合适

4. GeneralTask 异步任务框架是怎么设计的?怎么保证任务不冲突?

  • 考察点:异步编程、任务调度、并发控制、设计模式
  • 问题剖析:任务框架的核心挑战是"如何让 100+ 种任务类型共用一套调度逻辑"和"如何防止同类型任务并发执行"。常见误区是用 if-else 分发任务类型,或者忽略并发控制。
  • 思考过程:先说框架设计(策略模式 + Spring 自动注入),再说并发控制(互斥检查),最后说可观测性(进度追踪)。
  • 详细回答:
    ① 策略模式:定义 IGeneralTaskHandler 接口,每种任务类型实现一个 Handler,通过 getTaskType() 标识任务类型。Spring 自动注入所有实现类到 List<IGeneralTaskHandler>,运行时根据 taskType 匹配对应 Handler
    ② 异步执行:使用 @Async 注解 + 自定义线程池(核心 50、最大 100),任务创建后立即返回任务 ID,后台异步执行
    ③ 互斥控制:任务创建前检查是否有同类型任务正在执行(checkTaskMutex),防止同一批数据被并发处理导致数据不一致
    ④ 关账校验:所有写操作任务创建前检查目标月份是否已关账,已关账则拒绝执行
    ⑤ 进度追踪:任务执行过程中通过 updateTaskProgress() 更新进度百分比,前端可实时查看
    ⑥ 异常处理:Handler 执行异常时自动标记任务失败(markTaskFailed),记录错误信息,支持排查和重试

5. Flyway 管理 258+ 迁移脚本,实际操作中遇到过什么问题?

  • 考察点:数据库版本管理、Flyway 实战经验、多环境管理
  • 问题剖析:Flyway 在小项目中很简单,但在 14 个数据库、258+ 脚本的规模下,会遇到脚本冲突、环境差异、回滚困难等问题。面试官想听的是实战踩坑经验。
  • 思考过程:从脚本命名规范、多库管理、环境差异、回滚策略几个角度切入。
  • 详细回答:
    ① 脚本命名规范:采用 V{版本号}__{平台}_{描述}.sql 格式,按平台分目录管理(ebay/、aliexpress/、base/ 等),避免跨平台脚本冲突
    ② 多库管理:14 个数据库各自独立的 Flyway 配置,每个库有自己的 flyway_schema_history 表,互不干扰
    ③ 环境差异:开发环境(local)和线上环境的数据库名不同(如 codo_finance_ebay_dev vs codo_finance_ebay),通过 Spring Profile 区分配置
    ④ 常见问题:开发人员并行开发时版本号冲突,解决方案是用时间戳作为版本号前缀;偶尔出现脚本执行失败导致 Flyway 状态锁死,需要手动修复 flyway_schema_history 表
    ⑤ 回滚策略:Flyway 社区版不支持自动回滚,我们的做法是编写对应的 undo SQL 脚本手动执行,关键变更前先备份

6. 多币种核算是怎么处理的?汇率怎么管理?

  • 考察点:金融领域知识、精度处理、汇率管理
  • 问题剖析:多币种核算的核心问题是"用什么汇率、什么时间点的汇率、精度怎么保证"。常见误区是用 float/double 处理金额,或者忽略汇率时效性。
  • 思考过程:从汇率数据来源、存储方式、转换时机、精度控制几个层面回答。
  • 详细回答:
    ① 汇率管理:系统维护了 ExchangeRate 表,按核算月 + 币种存储汇率,财务人员每月初手动录入当月汇率(IExchangeRateService)
    ② 转换时机:在数据清洗阶段,将各币种金额统一转换为美元(USD)或人民币(CNY),转换后的金额存入独立字段(如 usd_amount)
    ③ 精度控制:金额字段使用 DECIMAL 类型存储,Java 层使用 BigDecimal 计算,避免浮点精度丢失
    ④ 币种枚举:CurrencyEnum 定义了所有支持的币种(USD、EUR、GBP、CNY 等),清洗时自动识别并标准化
    ⑤ 特殊处理:部分平台(如 eBay)的运费字段带有币种符号(如 "$12.5"),清洗时需要先剥离符号再转换

7. 数据清洗环节,去重策略是怎么设计的?为什么要分"跨月去重"和"月内去重"?

  • 考察点:数据质量管理、去重策略、业务理解
  • 问题剖析:去重看似简单,但在财务核算场景下有特殊要求:同一笔订单可能在不同月份的导入文件中重复出现(平台导出的时间范围有重叠),也可能在同一个文件中重复。两种重复的处理策略不同。
  • 思考过程:先解释为什么会重复(平台导出机制),再说两种去重的区别和顺序。
  • 详细回答:
    ① 重复来源:平台导出的财务数据通常按时间范围筛选,但时间边界不精确,导致相邻月份的导出文件有重叠;同一个文件内也可能因为平台 bug 出现重复行
    ② 跨月去重(优先执行):deleteWithHistoryDuplicateOrder 将本次导入的数据与历史月份已入库的数据比对,如果订单号已存在于历史数据中,则删除本次导入的重复记录。原则是"以历史数据为准",因为历史数据已经过核算确认
    ③ 月内去重(其次执行):deleteWithThisMonthDuplicateOrder 处理同一个文件或同月多次导入产生的重复,保留最新一条
    ④ 执行顺序很重要:必须先跨月再月内,否则月内去重可能保留了一条与历史重复的记录
    ⑤ 去重维度:以订单号 + 平台 + 店铺作为唯一标识,而不是简单的全字段比对

8. 系统怎么处理"本月结算了历史已调整订单"的场景?

  • 考察点:业务复杂度理解、自动冲减逻辑、数据一致性
  • 问题剖析:这是财务核算中最容易出错的场景之一。历史月份的订单可能已经做过差异调整(人工确认差异原因并记录),但本月平台又结算了这笔订单,如果不处理就会导致重复调整。
  • 思考过程:先描述场景,再说自动冲减的实现逻辑,最后说为什么不能简单忽略。
  • 详细回答:
    ① 场景描述:假设 3 月份订单 A 与发款存在 $ 5 差异,财务人员在 3 月做了 $5 的差异调整。到了 5 月,平台补发了这笔 $ 5,如果不处理,这 $5 就被"调整"了两次
    ② 自动冲减:对账流程的最后一步 autoInsertAdjustReverse,会自动检测本月结算中是否包含历史已调整的订单,如果有,自动插入一条反向的冲减记录到差异调整表
    ③ 实现方式:通过 JOIN 历史差异调整表和本月结算数据,找出重叠的订单,自动生成冲减金额 = 历史调整金额的负数
    ④ 为什么不能忽略:财务核算要求每一分钱都有出处,重复调整会导致账单金额虚高,影响客户结算和公司利润计算

9. BatchOps 批量操作工具是怎么设计的?为什么默认 500 条/批?

  • 考察点:批处理设计、数据库性能优化、参数调优
  • 问题剖析:批量操作的核心权衡是"批次大小"——太小则 SQL 执行次数多、网络开销大;太大则单次 SQL 过长、可能超过 MySQL 的 max_allowed_packet、事务锁持有时间长。
  • 思考过程:先说工具设计(函数式接口 + 分批执行),再说 500 这个数字的来源。
  • 详细回答:
    ① 设计:BatchOps.batchOps(Consumer<List<T>>, List<T>, int batchSize),接收一个消费函数和数据列表,自动按批次大小分割后逐批执行。使用 Java 函数式接口,调用方只需传入 lambda 表达式
    ② 500 条/批的依据:MySQL 默认 max_allowed_packet 为 4MB,单条记录按平均 1-2KB 估算,500 条约 500KB-1MB,留有充足余量;同时 500 条的 INSERT 语句执行时间通常在 100ms 以内,不会长时间持有事务锁
    ③ 支持自定义:batchOps 方法重载支持传入自定义批次大小,特殊场景(如字段特别多的表)可以调小
    ④ 使用场景:数据导入后的批量 INSERT、清洗后的批量 UPDATE、匹配结果的批量写入等

10. 关账机制是怎么实现的?怎么防止已核算数据被误改?

  • 考察点:数据安全、业务流程管控、架构设计
  • 问题剖析:关账是财务系统的核心管控机制,但实现上容易出现"漏网之鱼"——某些接口忘记加关账校验。面试官想听的是如何从架构层面保证,而不是靠开发人员自觉。
  • 思考过程:从关账的业务含义、技术实现、架构保障三个层面回答。
  • 详细回答:
    ① 业务含义:关账表示某个平台某个月份的核算已完成,数据已确认,不允许再修改。支持关账和取消关账两种操作
    ② 技术实现:FinanceCloseRecordService 维护关账记录表,关账时插入记录,取消关账时逻辑删除
    ③ 架构保障:所有写操作(导入、清洗、匹配、对账、调整)在任务创建时统一调用 checkFinanceClose() 前置校验,已关账则直接抛出业务异常拒绝执行
    ④ 统一入口:所有操作都通过 GeneralTaskService.createXxxTask() 创建任务,关账校验在这个统一入口中完成,不依赖各个 Handler 自行校验,从架构层面杜绝遗漏
    ⑤ 逻辑删除:取消关账使用逻辑删除而非物理删除,保留关账/取消关账的操作历史,便于审计

11. 如果让你重新设计这个系统,你会做哪些改进?

  • 考察点:架构反思能力、技术视野、持续改进意识
  • 问题剖析:这是一道开放题,面试官想看你是否能跳出当前实现,从更高维度思考系统的不足和改进方向。忌讳的是"我觉得现在挺好的"或者"全部推倒重来"。
  • 思考过程:从代码层面、架构层面、产品层面三个维度给出具体改进点。
  • 详细回答:
    ① 代码层面——模板方法抽象:各平台的 Clean/Match/Reconcile 有 60-70% 的公共逻辑,可以抽取为抽象基类,用模板方法模式定义流程骨架,平台差异部分用钩子方法实现,减少代码重复
    ② 架构层面——引入消息队列:用 Kafka 解耦导入和处理流程,导入完成后发消息触发清洗,清洗完成后触发匹配,形成事件驱动的 Pipeline,支持更灵活的流程编排和更大的数据吞吐量
    ③ 架构层面——数据质量监控:在清洗和匹配环节增加数据质量指标的自动采集(匹配率、异常率、去重率),接入告警系统,异常时自动通知
    ④ 产品层面——可视化对账看板:将对账结果以可视化图表展示(差异分布、趋势变化),替代目前的纯表格导出,提升财务人员的使用体验
    ⑤ 运维层面——动态数据源:14 个数据库的连接池可以按使用频率动态调整,非核算期间缩小连接池,核算期间自动扩容

12. 你在这个项目中遇到的最大技术挑战是什么?

  • 考察点:问题解决能力、技术深度、真实项目经验
  • 问题剖析:面试官想听具体的、有细节的挑战,而不是泛泛而谈。最好能说出"问题是什么→怎么排查→怎么解决→效果如何"的完整链路。
  • 思考过程:选一个有代表性的技术挑战,按"发现问题→分析原因→解决方案→效果验证"的结构回答。
  • 详细回答:
    ① 最大挑战是多级匹配策略的设计和调优。最初只用订单号单一维度匹配,覆盖率只有 ~70%,大量发款数据匹配不到客户,财务人员需要手工处理
    ② 排查过程:分析未匹配数据的特征,发现三类情况——有些发款只有 Item Number 没有订单号,有些有自定义标签但格式不规范,有些是历史订单已过期
    ③ 解决方案:设计了 5 级回退匹配策略,从最精确的自定义标签匹配开始,逐级降级到 Item Number、订单号等维度,每一级都有明确的前置条件防止误匹配
    ④ 调优过程:上线后持续观察匹配率指标,针对特殊 case 增加了客户名称合并规则(如"新捷丽A/B"→"新捷丽")和仓库 SKU 到产品 SKU 的二次匹配
    ⑤ 最终效果:匹配覆盖率从 ~70% 提升到 95%+,剩余 5% 为真正的异常数据需要人工确认,财务人员的手工处理量减少了 80%+

10. 简历提炼版

  • 主导设计并开发集团多平台财务核算系统,覆盖 eBay、AliExpress、Shopee、Walmart 等 13+ 电商平台,采用多数据源隔离架构(14 个独立数据库 + 258+ Flyway 迁移脚本),实现数据导入→清洗→匹配→对账→关账全流程自动化
  • 设计 5 级回退匹配算法(自定义标签→订单号+ItemNumber→ItemNumber→订单号→特殊规则),将订单-客户匹配覆盖率从 ~70% 提升至 95%+;基于临时表 Pipeline 实现多步可审计对账,对账准确率达 99%+
  • 封装 GeneralTask 异步任务框架(策略模式 + 100+ 任务类型),支持任务互斥、关账校验、进度追踪;整体核算效率提升 10 倍+(单平台从 2-3 天缩短至 10-30 分钟),财务人力从 3-5 人降至 1 人
相关阅读

8 分钟

Creeks 系统线程池与多线程使用分析

整个系统使用了 **3 种多线程模式**,涉及 **4 个独立的线程池配置类**,覆盖 **定时任务、并行查询、异步通知、报告处理** 等场景。 假设服务器 8 核 CPU: Amazon 同步服务选 是明智的——调用者自己跑任务,自动降速

面试#其他8 阅读 · 0 评论

21 分钟

多线程与JUC并发编程实战

Creeks ERP 系统是一套跨境电商全链路管理平台,涵盖库存(inventory)、销售(sales)、履约(fulfillment)、财务(finance)、平台同步(esync)等十余个微服务模块。系统对接了 eBay、Amazon

面试#其他8 阅读 · 0 评论

评论需要登录。生产环境使用 GitHub 登录,本地可前往 登录页 使用开发登录。

还没有评论,来抢沙发。