财务对账系统设计与实现

财务对账系统设计与实现:支付宝、微信、银行流水全渠道对账

前言

在电商、金融等业务场景中,对账系统是保障资金安全的核心环节。本文将详细介绍如何设计和实现一套支持支付宝、微信、银行等多渠道的财务对账系统,涵盖系统架构、核心算法、代码实现等内容。


一、对账系统概述

1.1 什么是对账

对账是指将企业内部的交易记录与外部渠道(支付宝、微信、银行等)的流水记录进行核对,确保双方数据一致的过程。

lua 复制代码
+------------------------------------------------------------------+
|                        对账核心流程                                |
+------------------------------------------------------------------+
|                                                                   |
|   内部系统                                       外部渠道          |
|   +------------------+                    +------------------+    |
|   |   交易订单表     |                    |   支付宝账单     |    |
|   |   支付流水表     |      对账核心      |   微信账单       |    |
|   |   退款记录表     |  <--------------> |   银行流水       |    |
|   +------------------+                    |   自定义流水     |    |
|                                           +------------------+    |
|                             |                                     |
|                             v                                     |
|                    +------------------+                           |
|                    |    对账结果      |                           |
|                    +------------------+                           |
|                    | - 对账成功       |                           |
|                    | - 本地有渠道无   |                           |
|                    | - 渠道有本地无   |                           |
|                    | - 金额不一致     |                           |
|                    +------------------+                           |
|                                                                   |
+------------------------------------------------------------------+

1.2 对账的目的

  1. 资金安全:及时发现异常交易,防止资金损失
  2. 账务准确:确保财务报表数据准确
  3. 差错处理:快速定位和处理对账差异
  4. 风险控制:发现潜在的欺诈和风险

1.3 对账差异类型

lua 复制代码
+------------------------------------------------------------------+
|                        对账差异分类                                |
+------------------------------------------------------------------+
|                                                                   |
|   +--------------------+------------------------------------+     |
|   |     差异类型       |              描述                  |     |
|   +--------------------+------------------------------------+     |
|   |   本地多(长款)   |  本地有记录,渠道无记录             |     |
|   +--------------------+------------------------------------+     |
|   |   渠道多(短款)   |  渠道有记录,本地无记录             |     |
|   +--------------------+------------------------------------+     |
|   |   金额不一致       |  双方都有记录,但金额不同           |     |
|   +--------------------+------------------------------------+     |
|   |   状态不一致       |  双方金额相同,但交易状态不同       |     |
|   +--------------------+------------------------------------+     |
|   |   时间差异         |  交易时间跨天导致的差异             |     |
|   +--------------------+------------------------------------+     |
|                                                                   |
+------------------------------------------------------------------+

二、系统架构设计

2.1 整体架构

lua 复制代码
+------------------------------------------------------------------------+
|                          对账系统整体架构                                |
+------------------------------------------------------------------------+
|                                                                         |
|   +-------------------+    +-------------------+    +------------------+ |
|   |    定时任务       |    |    手动触发       |    |    文件上传      | |
|   +--------+----------+    +--------+----------+    +--------+---------+ |
|            |                        |                        |          |
|            +------------------------+------------------------+          |
|                                     |                                   |
|                                     v                                   |
|   +---------------------------------------------------------------------+
|   |                          对账调度中心                                |
|   +---------------------------------------------------------------------+
|                                     |                                   |
|            +------------------------+------------------------+          |
|            |                        |                        |          |
|            v                        v                        v          |
|   +----------------+       +----------------+       +----------------+  |
|   | 支付宝对账模块 |       | 微信对账模块   |       | 银行对账模块   |  |
|   +----------------+       +----------------+       +----------------+  |
|            |                        |                        |          |
|            +------------------------+------------------------+          |
|                                     |                                   |
|                                     v                                   |
|   +---------------------------------------------------------------------+
|   |                          对账引擎核心                                |
|   |  +-------------+  +-------------+  +-------------+  +-------------+ |
|   |  | 文件解析器  |  | 数据清洗    |  | 匹配算法    |  | 差异处理    | |
|   |  +-------------+  +-------------+  +-------------+  +-------------+ |
|   +---------------------------------------------------------------------+
|                                     |                                   |
|                                     v                                   |
|   +---------------------------------------------------------------------+
|   |                          数据存储层                                  |
|   |  +-------------+  +-------------+  +-------------+  +-------------+ |
|   |  | 对账任务表  |  | 渠道流水表  |  | 对账结果表  |  | 差异记录表  | |
|   |  +-------------+  +-------------+  +-------------+  +-------------+ |
|   +---------------------------------------------------------------------+
|                                     |                                   |
|                                     v                                   |
|   +---------------------------------------------------------------------+
|   |                          监控告警层                                  |
|   |  +------------------+  +------------------+  +-------------------+  |
|   |  | 对账进度监控     |  | 差异率告警       |  | 异常通知          |  |
|   |  +------------------+  +------------------+  +-------------------+  |
|   +---------------------------------------------------------------------+
|                                                                         |
+------------------------------------------------------------------------+

2.2 核心模块说明

diff 复制代码
+------------------------------------------------------------------+
|                        核心模块职责                                |
+------------------------------------------------------------------+
|                                                                   |
|   对账调度中心:                                                   |
|   - 管理对账任务的创建、调度、执行                                 |
|   - 支持定时对账和手动触发                                        |
|   - 任务状态管理和失败重试                                        |
|                                                                   |
|   文件解析器:                                                     |
|   - 支持 CSV、Excel、TXT 等格式                                   |
|   - 适配不同渠道的账单格式                                        |
|   - 大文件分片解析                                                |
|                                                                   |
|   匹配算法:                                                       |
|   - 单字段匹配(订单号)                                          |
|   - 多字段联合匹配                                                |
|   - 模糊匹配和容差匹配                                            |
|                                                                   |
|   差异处理:                                                       |
|   - 差异分类和标记                                                |
|   - 自动处理规则                                                  |
|   - 人工审核流程                                                  |
|                                                                   |
+------------------------------------------------------------------+

三、数据库设计

3.1 核心表结构

sql 复制代码
-- 1. 对账任务表
CREATE TABLE `recon_task` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    `task_no` VARCHAR(32) NOT NULL COMMENT '任务编号',
    `channel_type` VARCHAR(20) NOT NULL COMMENT '渠道类型:ALIPAY/WECHAT/BANK/CUSTOM',
    `recon_date` DATE NOT NULL COMMENT '对账日期',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待处理,1-处理中,2-已完成,3-失败',
    `total_count` INT DEFAULT 0 COMMENT '总记录数',
    `success_count` INT DEFAULT 0 COMMENT '对账成功数',
    `fail_count` INT DEFAULT 0 COMMENT '对账失败数',
    `diff_count` INT DEFAULT 0 COMMENT '差异数',
    `file_path` VARCHAR(500) COMMENT '账单文件路径',
    `error_msg` VARCHAR(1000) COMMENT '错误信息',
    `start_time` DATETIME COMMENT '开始时间',
    `end_time` DATETIME COMMENT '结束时间',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY `uk_task_no` (`task_no`),
    KEY `idx_channel_date` (`channel_type`, `recon_date`),
    KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对账任务表';

-- 2. 渠道流水表(支付宝)
CREATE TABLE `channel_bill_alipay` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `task_id` BIGINT NOT NULL COMMENT '任务ID',
    `trade_no` VARCHAR(64) NOT NULL COMMENT '支付宝交易号',
    `out_trade_no` VARCHAR(64) COMMENT '商户订单号',
    `trade_type` VARCHAR(20) COMMENT '交易类型',
    `trade_status` VARCHAR(20) COMMENT '交易状态',
    `amount` DECIMAL(12,2) NOT NULL COMMENT '交易金额',
    `fee` DECIMAL(12,2) DEFAULT 0 COMMENT '手续费',
    `trade_time` DATETIME COMMENT '交易时间',
    `subject` VARCHAR(256) COMMENT '商品名称',
    `recon_status` TINYINT DEFAULT 0 COMMENT '对账状态:0-待对账,1-已对账,2-差异',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    KEY `idx_task_id` (`task_id`),
    KEY `idx_trade_no` (`trade_no`),
    KEY `idx_out_trade_no` (`out_trade_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付宝账单流水';

-- 3. 渠道流水表(微信)
CREATE TABLE `channel_bill_wechat` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `task_id` BIGINT NOT NULL COMMENT '任务ID',
    `transaction_id` VARCHAR(64) NOT NULL COMMENT '微信支付订单号',
    `out_trade_no` VARCHAR(64) COMMENT '商户订单号',
    `trade_type` VARCHAR(20) COMMENT '交易类型',
    `trade_state` VARCHAR(20) COMMENT '交易状态',
    `amount` DECIMAL(12,2) NOT NULL COMMENT '交易金额',
    `fee` DECIMAL(12,2) DEFAULT 0 COMMENT '手续费',
    `trade_time` DATETIME COMMENT '交易时间',
    `body` VARCHAR(256) COMMENT '商品描述',
    `recon_status` TINYINT DEFAULT 0 COMMENT '对账状态',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    KEY `idx_task_id` (`task_id`),
    KEY `idx_transaction_id` (`transaction_id`),
    KEY `idx_out_trade_no` (`out_trade_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信账单流水';

-- 4. 渠道流水表(银行)
CREATE TABLE `channel_bill_bank` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `task_id` BIGINT NOT NULL COMMENT '任务ID',
    `bank_serial_no` VARCHAR(64) NOT NULL COMMENT '银行流水号',
    `merchant_order_no` VARCHAR(64) COMMENT '商户订单号',
    `trans_type` VARCHAR(20) COMMENT '交易类型:DEBIT/CREDIT',
    `trans_status` VARCHAR(20) COMMENT '交易状态',
    `amount` DECIMAL(12,2) NOT NULL COMMENT '交易金额',
    `fee` DECIMAL(12,2) DEFAULT 0 COMMENT '手续费',
    `trans_time` DATETIME COMMENT '交易时间',
    `remark` VARCHAR(256) COMMENT '备注',
    `account_no` VARCHAR(32) COMMENT '账户号',
    `account_name` VARCHAR(64) COMMENT '账户名',
    `recon_status` TINYINT DEFAULT 0 COMMENT '对账状态',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    KEY `idx_task_id` (`task_id`),
    KEY `idx_bank_serial_no` (`bank_serial_no`),
    KEY `idx_merchant_order_no` (`merchant_order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='银行账单流水';

-- 5. 自定义渠道流水表
CREATE TABLE `channel_bill_custom` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `task_id` BIGINT NOT NULL COMMENT '任务ID',
    `channel_code` VARCHAR(32) NOT NULL COMMENT '渠道编码',
    `channel_trade_no` VARCHAR(64) NOT NULL COMMENT '渠道交易号',
    `merchant_order_no` VARCHAR(64) COMMENT '商户订单号',
    `trade_type` VARCHAR(20) COMMENT '交易类型',
    `trade_status` VARCHAR(20) COMMENT '交易状态',
    `amount` DECIMAL(12,2) NOT NULL COMMENT '交易金额',
    `fee` DECIMAL(12,2) DEFAULT 0 COMMENT '手续费',
    `trade_time` DATETIME COMMENT '交易时间',
    `ext_data` JSON COMMENT '扩展数据',
    `recon_status` TINYINT DEFAULT 0 COMMENT '对账状态',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    KEY `idx_task_id` (`task_id`),
    KEY `idx_channel_trade_no` (`channel_code`, `channel_trade_no`),
    KEY `idx_merchant_order_no` (`merchant_order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='自定义渠道流水';

-- 6. 本地支付流水表
CREATE TABLE `local_payment_record` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `order_no` VARCHAR(64) NOT NULL COMMENT '商户订单号',
    `channel_type` VARCHAR(20) NOT NULL COMMENT '支付渠道',
    `channel_trade_no` VARCHAR(64) COMMENT '渠道交易号',
    `pay_type` VARCHAR(20) COMMENT '支付类型',
    `pay_status` TINYINT NOT NULL COMMENT '支付状态:0-待支付,1-支付成功,2-支付失败,3-已退款',
    `amount` DECIMAL(12,2) NOT NULL COMMENT '支付金额',
    `fee` DECIMAL(12,2) DEFAULT 0 COMMENT '手续费',
    `pay_time` DATETIME COMMENT '支付时间',
    `user_id` BIGINT COMMENT '用户ID',
    `product_name` VARCHAR(256) COMMENT '商品名称',
    `recon_status` TINYINT DEFAULT 0 COMMENT '对账状态:0-待对账,1-已对账,2-差异',
    `recon_time` DATETIME COMMENT '对账时间',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY `uk_order_no` (`order_no`),
    KEY `idx_channel_trade_no` (`channel_type`, `channel_trade_no`),
    KEY `idx_pay_time` (`pay_time`),
    KEY `idx_recon_status` (`recon_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本地支付流水表';

-- 7. 对账结果表
CREATE TABLE `recon_result` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `task_id` BIGINT NOT NULL COMMENT '任务ID',
    `order_no` VARCHAR(64) COMMENT '商户订单号',
    `channel_trade_no` VARCHAR(64) COMMENT '渠道交易号',
    `result_type` TINYINT NOT NULL COMMENT '结果类型:1-匹配成功,2-本地多,3-渠道多,4-金额不一致,5-状态不一致',
    `local_amount` DECIMAL(12,2) COMMENT '本地金额',
    `channel_amount` DECIMAL(12,2) COMMENT '渠道金额',
    `diff_amount` DECIMAL(12,2) COMMENT '差异金额',
    `local_status` VARCHAR(20) COMMENT '本地状态',
    `channel_status` VARCHAR(20) COMMENT '渠道状态',
    `handle_status` TINYINT DEFAULT 0 COMMENT '处理状态:0-待处理,1-已处理,2-忽略',
    `handle_remark` VARCHAR(500) COMMENT '处理备注',
    `handler` VARCHAR(64) COMMENT '处理人',
    `handle_time` DATETIME COMMENT '处理时间',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    KEY `idx_task_id` (`task_id`),
    KEY `idx_order_no` (`order_no`),
    KEY `idx_result_type` (`result_type`),
    KEY `idx_handle_status` (`handle_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对账结果表';

-- 8. 渠道配置表
CREATE TABLE `recon_channel_config` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `channel_code` VARCHAR(32) NOT NULL COMMENT '渠道编码',
    `channel_name` VARCHAR(64) NOT NULL COMMENT '渠道名称',
    `file_type` VARCHAR(20) COMMENT '文件类型:CSV/EXCEL/TXT',
    `file_encoding` VARCHAR(20) DEFAULT 'UTF-8' COMMENT '文件编码',
    `field_mapping` JSON COMMENT '字段映射配置',
    `parser_class` VARCHAR(256) COMMENT '解析器类名',
    `status` TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY `uk_channel_code` (`channel_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='渠道配置表';

3.2 E-R 关系图

lua 复制代码
+------------------------------------------------------------------+
|                          E-R 关系图                               |
+------------------------------------------------------------------+
|                                                                   |
|   +-------------+       1:N       +-------------------+           |
|   | recon_task  |---------------->| channel_bill_xxx |           |
|   +-------------+                 +-------------------+           |
|         |                                                         |
|         | 1:N                                                     |
|         v                                                         |
|   +-------------+                 +---------------------+         |
|   | recon_result|<--------------->| local_payment_record|         |
|   +-------------+      关联       +---------------------+         |
|                                                                   |
|   +---------------------+                                         |
|   | recon_channel_config|  渠道配置(独立)                        |
|   +---------------------+                                         |
|                                                                   |
+------------------------------------------------------------------+

四、核心代码实现

4.1 基础实体类

java 复制代码
/**
 * 渠道类型枚举
 */
public enum ChannelType {
    ALIPAY("ALIPAY", "支付宝"),
    WECHAT("WECHAT", "微信支付"),
    BANK("BANK", "银行"),
    CUSTOM("CUSTOM", "自定义渠道");

    private final String code;
    private final String desc;

    ChannelType(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public String getCode() { return code; }
    public String getDesc() { return desc; }
}

/**
 * 对账结果类型枚举
 */
public enum ReconResultType {
    MATCHED(1, "匹配成功"),
    LOCAL_MORE(2, "本地多(长款)"),
    CHANNEL_MORE(3, "渠道多(短款)"),
    AMOUNT_MISMATCH(4, "金额不一致"),
    STATUS_MISMATCH(5, "状态不一致");

    private final int code;
    private final String desc;

    ReconResultType(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() { return code; }
    public String getDesc() { return desc; }
}

/**
 * 对账任务状态枚举
 */
public enum TaskStatus {
    PENDING(0, "待处理"),
    PROCESSING(1, "处理中"),
    COMPLETED(2, "已完成"),
    FAILED(3, "失败");

    private final int code;
    private final String desc;

    TaskStatus(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() { return code; }
    public String getDesc() { return desc; }
}
java 复制代码
/**
 * 对账任务实体
 */
@Data
@TableName("recon_task")
public class ReconTask {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String taskNo;
    private String channelType;
    private LocalDate reconDate;
    private Integer status;
    private Integer totalCount;
    private Integer successCount;
    private Integer failCount;
    private Integer diffCount;
    private String filePath;
    private String errorMsg;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

/**
 * 统一渠道账单数据模型
 */
@Data
public class ChannelBillDTO {
    private String channelTradeNo;      // 渠道交易号
    private String merchantOrderNo;     // 商户订单号
    private String tradeType;           // 交易类型
    private String tradeStatus;         // 交易状态
    private BigDecimal amount;          // 交易金额
    private BigDecimal fee;             // 手续费
    private LocalDateTime tradeTime;    // 交易时间
    private String remark;              // 备注
    private Map<String, Object> extData;// 扩展数据
}

/**
 * 本地支付记录实体
 */
@Data
@TableName("local_payment_record")
public class LocalPaymentRecord {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String orderNo;
    private String channelType;
    private String channelTradeNo;
    private String payType;
    private Integer payStatus;
    private BigDecimal amount;
    private BigDecimal fee;
    private LocalDateTime payTime;
    private Long userId;
    private String productName;
    private Integer reconStatus;
    private LocalDateTime reconTime;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

/**
 * 对账结果实体
 */
@Data
@TableName("recon_result")
public class ReconResult {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long taskId;
    private String orderNo;
    private String channelTradeNo;
    private Integer resultType;
    private BigDecimal localAmount;
    private BigDecimal channelAmount;
    private BigDecimal diffAmount;
    private String localStatus;
    private String channelStatus;
    private Integer handleStatus;
    private String handleRemark;
    private String handler;
    private LocalDateTime handleTime;
    private LocalDateTime createTime;
}

4.2 文件解析器

java 复制代码
/**
 * 账单解析器接口
 */
public interface BillParser {

    /**
     * 获取支持的渠道类型
     */
    ChannelType getChannelType();

    /**
     * 解析账单文件
     */
    List<ChannelBillDTO> parse(InputStream inputStream, String fileName) throws Exception;

    /**
     * 是否支持该文件
     */
    boolean supports(String fileName);
}

/**
 * 支付宝账单解析器
 */
@Slf4j
@Component
public class AlipayBillParser implements BillParser {

    @Override
    public ChannelType getChannelType() {
        return ChannelType.ALIPAY;
    }

    @Override
    public boolean supports(String fileName) {
        return fileName != null && fileName.contains("alipay");
    }

    @Override
    public List<ChannelBillDTO> parse(InputStream inputStream, String fileName) throws Exception {
        List<ChannelBillDTO> billList = new ArrayList<>();

        // 支付宝账单通常是 CSV 格式,GBK 编码
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(inputStream, "GBK"))) {

            String line;
            boolean isDataSection = false;
            int lineNum = 0;

            while ((line = reader.readLine()) != null) {
                lineNum++;

                // 跳过账单头部信息(支付宝账单前几行是汇总信息)
                if (line.startsWith("支付宝交易号")) {
                    isDataSection = true;
                    continue;
                }

                // 跳过汇总行
                if (line.startsWith("#") || line.trim().isEmpty()) {
                    continue;
                }

                if (!isDataSection) {
                    continue;
                }

                try {
                    ChannelBillDTO bill = parseLine(line);
                    if (bill != null) {
                        billList.add(bill);
                    }
                } catch (Exception e) {
                    log.warn("解析支付宝账单第{}行失败: {}", lineNum, e.getMessage());
                }
            }
        }

        log.info("支付宝账单解析完成,共{}条记录", billList.size());
        return billList;
    }

    private ChannelBillDTO parseLine(String line) {
        // 支付宝账单字段:支付宝交易号,商户订单号,交易类型,交易状态,金额,手续费,交易时间...
        String[] fields = line.split(",");
        if (fields.length < 7) {
            return null;
        }

        ChannelBillDTO bill = new ChannelBillDTO();
        bill.setChannelTradeNo(fields[0].trim());
        bill.setMerchantOrderNo(fields[1].trim());
        bill.setTradeType(fields[2].trim());
        bill.setTradeStatus(fields[3].trim());
        bill.setAmount(new BigDecimal(fields[4].trim()));
        bill.setFee(new BigDecimal(fields[5].trim()));

        // 解析时间
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        bill.setTradeTime(LocalDateTime.parse(fields[6].trim(), formatter));

        return bill;
    }
}

/**
 * 微信账单解析器
 */
@Slf4j
@Component
public class WechatBillParser implements BillParser {

    @Override
    public ChannelType getChannelType() {
        return ChannelType.WECHAT;
    }

    @Override
    public boolean supports(String fileName) {
        return fileName != null && fileName.contains("wechat");
    }

    @Override
    public List<ChannelBillDTO> parse(InputStream inputStream, String fileName) throws Exception {
        List<ChannelBillDTO> billList = new ArrayList<>();

        // 微信账单是 CSV 格式,UTF-8 编码
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {

            String line;
            boolean isDataSection = false;
            int lineNum = 0;

            while ((line = reader.readLine()) != null) {
                lineNum++;

                // 微信账单以"交易时间"开头的是表头
                if (line.startsWith("交易时间")) {
                    isDataSection = true;
                    continue;
                }

                // 跳过汇总部分
                if (line.startsWith("总交易单数") || line.trim().isEmpty()) {
                    isDataSection = false;
                    continue;
                }

                if (!isDataSection) {
                    continue;
                }

                try {
                    ChannelBillDTO bill = parseLine(line);
                    if (bill != null) {
                        billList.add(bill);
                    }
                } catch (Exception e) {
                    log.warn("解析微信账单第{}行失败: {}", lineNum, e.getMessage());
                }
            }
        }

        log.info("微信账单解析完成,共{}条记录", billList.size());
        return billList;
    }

    private ChannelBillDTO parseLine(String line) {
        // 微信账单字段:交易时间,公众账号ID,商户号,子商户号,设备号,微信订单号,商户订单号,用户标识,交易类型,交易状态,付款银行,货币种类,总金额,企业红包金额,微信退款单号,商户退款单号,退款金额,企业红包退款金额,退款类型,退款状态,商品名称,商户数据包,手续费,费率
        String[] fields = line.split(",");
        if (fields.length < 15) {
            return null;
        }

        ChannelBillDTO bill = new ChannelBillDTO();

        // 去除反引号(微信账单中数字字段带反引号)
        bill.setChannelTradeNo(cleanField(fields[5]));
        bill.setMerchantOrderNo(cleanField(fields[6]));
        bill.setTradeType(cleanField(fields[8]));
        bill.setTradeStatus(cleanField(fields[9]));

        // 金额字段需要去除 ¥ 符号
        String amountStr = cleanField(fields[12]).replace("¥", "");
        bill.setAmount(new BigDecimal(amountStr));

        // 手续费
        if (fields.length > 22) {
            String feeStr = cleanField(fields[22]).replace("¥", "");
            bill.setFee(new BigDecimal(feeStr));
        }

        // 解析时间
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        bill.setTradeTime(LocalDateTime.parse(cleanField(fields[0]), formatter));

        return bill;
    }

    private String cleanField(String field) {
        return field.replace("`", "").trim();
    }
}

/**
 * 银行账单解析器(Excel 格式)
 */
@Slf4j
@Component
public class BankBillParser implements BillParser {

    @Override
    public ChannelType getChannelType() {
        return ChannelType.BANK;
    }

    @Override
    public boolean supports(String fileName) {
        return fileName != null &&
                (fileName.endsWith(".xlsx") || fileName.endsWith(".xls"));
    }

    @Override
    public List<ChannelBillDTO> parse(InputStream inputStream, String fileName) throws Exception {
        List<ChannelBillDTO> billList = new ArrayList<>();

        // 使用 EasyExcel 解析
        EasyExcel.read(inputStream, BankBillExcelDTO.class, new ReadListener<BankBillExcelDTO>() {

            @Override
            public void invoke(BankBillExcelDTO data, AnalysisContext context) {
                ChannelBillDTO bill = convertToChannelBill(data);
                if (bill != null) {
                    billList.add(bill);
                }
            }

            @Override
            public void doAfterAllAnalysed(AnalysisContext context) {
                log.info("银行账单解析完成,共{}条记录", billList.size());
            }
        }).sheet().doRead();

        return billList;
    }

    private ChannelBillDTO convertToChannelBill(BankBillExcelDTO excelDTO) {
        ChannelBillDTO bill = new ChannelBillDTO();
        bill.setChannelTradeNo(excelDTO.getBankSerialNo());
        bill.setMerchantOrderNo(excelDTO.getMerchantOrderNo());
        bill.setTradeType(excelDTO.getTransType());
        bill.setTradeStatus(excelDTO.getTransStatus());
        bill.setAmount(excelDTO.getAmount());
        bill.setFee(excelDTO.getFee());
        bill.setTradeTime(excelDTO.getTransTime());
        bill.setRemark(excelDTO.getRemark());
        return bill;
    }

    /**
     * 银行账单 Excel DTO
     */
    @Data
    public static class BankBillExcelDTO {
        @ExcelProperty("流水号")
        private String bankSerialNo;

        @ExcelProperty("商户订单号")
        private String merchantOrderNo;

        @ExcelProperty("交易类型")
        private String transType;

        @ExcelProperty("交易状态")
        private String transStatus;

        @ExcelProperty("交易金额")
        private BigDecimal amount;

        @ExcelProperty("手续费")
        private BigDecimal fee;

        @ExcelProperty("交易时间")
        private LocalDateTime transTime;

        @ExcelProperty("备注")
        private String remark;
    }
}

/**
 * 自定义渠道解析器工厂
 */
@Slf4j
@Component
public class CustomBillParserFactory {

    @Autowired
    private ReconChannelConfigMapper channelConfigMapper;

    /**
     * 根据渠道配置动态解析账单
     */
    public List<ChannelBillDTO> parse(String channelCode, InputStream inputStream) throws Exception {
        ReconChannelConfig config = channelConfigMapper.selectByChannelCode(channelCode);
        if (config == null) {
            throw new RuntimeException("渠道配置不存在: " + channelCode);
        }

        // 解析字段映射配置
        Map<String, String> fieldMapping = JSON.parseObject(
                config.getFieldMapping(), new TypeReference<Map<String, String>>() {});

        List<ChannelBillDTO> billList = new ArrayList<>();

        // 根据文件类型解析
        if ("CSV".equalsIgnoreCase(config.getFileType())) {
            billList = parseCsv(inputStream, config.getFileEncoding(), fieldMapping);
        } else if ("EXCEL".equalsIgnoreCase(config.getFileType())) {
            billList = parseExcel(inputStream, fieldMapping);
        }

        return billList;
    }

    private List<ChannelBillDTO> parseCsv(InputStream inputStream, String encoding,
                                           Map<String, String> fieldMapping) throws Exception {
        List<ChannelBillDTO> billList = new ArrayList<>();

        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(inputStream, encoding))) {

            // 读取表头
            String headerLine = reader.readLine();
            String[] headers = headerLine.split(",");

            // 构建列索引映射
            Map<String, Integer> columnIndex = new HashMap<>();
            for (int i = 0; i < headers.length; i++) {
                columnIndex.put(headers[i].trim(), i);
            }

            String line;
            while ((line = reader.readLine()) != null) {
                if (line.trim().isEmpty()) continue;

                String[] fields = line.split(",");
                ChannelBillDTO bill = new ChannelBillDTO();

                // 根据字段映射配置填充数据
                for (Map.Entry<String, String> entry : fieldMapping.entrySet()) {
                    String targetField = entry.getKey();  // ChannelBillDTO 字段名
                    String sourceColumn = entry.getValue(); // 源文件列名

                    Integer idx = columnIndex.get(sourceColumn);
                    if (idx != null && idx < fields.length) {
                        setFieldValue(bill, targetField, fields[idx].trim());
                    }
                }

                billList.add(bill);
            }
        }

        return billList;
    }

    private void setFieldValue(ChannelBillDTO bill, String fieldName, String value) {
        switch (fieldName) {
            case "channelTradeNo":
                bill.setChannelTradeNo(value);
                break;
            case "merchantOrderNo":
                bill.setMerchantOrderNo(value);
                break;
            case "tradeType":
                bill.setTradeType(value);
                break;
            case "tradeStatus":
                bill.setTradeStatus(value);
                break;
            case "amount":
                bill.setAmount(new BigDecimal(value));
                break;
            case "fee":
                bill.setFee(new BigDecimal(value));
                break;
            case "tradeTime":
                bill.setTradeTime(LocalDateTime.parse(value,
                        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
                break;
        }
    }

    private List<ChannelBillDTO> parseExcel(InputStream inputStream,
                                             Map<String, String> fieldMapping) {
        // Excel 解析逻辑,类似 CSV
        return new ArrayList<>();
    }
}

4.3 对账引擎核心

java 复制代码
/**
 * 对账引擎接口
 */
public interface ReconEngine {

    /**
     * 执行对账
     */
    ReconResult reconcile(ChannelBillDTO channelBill, LocalPaymentRecord localRecord);

    /**
     * 批量对账
     */
    List<ReconResult> batchReconcile(List<ChannelBillDTO> channelBills,
                                      List<LocalPaymentRecord> localRecords);
}

/**
 * 对账引擎实现
 */
@Slf4j
@Service
public class DefaultReconEngine implements ReconEngine {

    // 金额容差(分)
    private static final BigDecimal AMOUNT_TOLERANCE = new BigDecimal("0.01");

    @Override
    public ReconResult reconcile(ChannelBillDTO channelBill, LocalPaymentRecord localRecord) {
        ReconResult result = new ReconResult();
        result.setChannelTradeNo(channelBill.getChannelTradeNo());
        result.setChannelAmount(channelBill.getAmount());
        result.setChannelStatus(channelBill.getTradeStatus());

        if (localRecord != null) {
            result.setOrderNo(localRecord.getOrderNo());
            result.setLocalAmount(localRecord.getAmount());
            result.setLocalStatus(String.valueOf(localRecord.getPayStatus()));

            // 比对金额
            BigDecimal diff = channelBill.getAmount()
                    .subtract(localRecord.getAmount()).abs();

            if (diff.compareTo(AMOUNT_TOLERANCE) > 0) {
                // 金额不一致
                result.setResultType(ReconResultType.AMOUNT_MISMATCH.getCode());
                result.setDiffAmount(diff);
            } else if (!isStatusMatched(channelBill.getTradeStatus(),
                    localRecord.getPayStatus())) {
                // 状态不一致
                result.setResultType(ReconResultType.STATUS_MISMATCH.getCode());
            } else {
                // 匹配成功
                result.setResultType(ReconResultType.MATCHED.getCode());
            }
        } else {
            // 渠道有,本地无(短款)
            result.setResultType(ReconResultType.CHANNEL_MORE.getCode());
        }

        return result;
    }

    @Override
    public List<ReconResult> batchReconcile(List<ChannelBillDTO> channelBills,
                                             List<LocalPaymentRecord> localRecords) {
        List<ReconResult> results = new ArrayList<>();

        // 构建本地记录索引(按商户订单号)
        Map<String, LocalPaymentRecord> localRecordMap = localRecords.stream()
                .collect(Collectors.toMap(
                        LocalPaymentRecord::getOrderNo,
                        r -> r,
                        (r1, r2) -> r1
                ));

        // 构建渠道记录索引
        Map<String, ChannelBillDTO> channelBillMap = channelBills.stream()
                .collect(Collectors.toMap(
                        ChannelBillDTO::getMerchantOrderNo,
                        b -> b,
                        (b1, b2) -> b1
                ));

        Set<String> matchedOrderNos = new HashSet<>();

        // 遍历渠道账单进行匹配
        for (ChannelBillDTO channelBill : channelBills) {
            String orderNo = channelBill.getMerchantOrderNo();
            LocalPaymentRecord localRecord = localRecordMap.get(orderNo);

            ReconResult result = reconcile(channelBill, localRecord);
            results.add(result);

            if (localRecord != null) {
                matchedOrderNos.add(orderNo);
            }
        }

        // 检查本地多的记录(长款)
        for (LocalPaymentRecord localRecord : localRecords) {
            if (!matchedOrderNos.contains(localRecord.getOrderNo())) {
                ReconResult result = new ReconResult();
                result.setOrderNo(localRecord.getOrderNo());
                result.setLocalAmount(localRecord.getAmount());
                result.setLocalStatus(String.valueOf(localRecord.getPayStatus()));
                result.setResultType(ReconResultType.LOCAL_MORE.getCode());
                results.add(result);
            }
        }

        return results;
    }

    /**
     * 状态匹配判断
     */
    private boolean isStatusMatched(String channelStatus, Integer localStatus) {
        // 渠道状态映射到本地状态
        // 支付宝:TRADE_SUCCESS -> 1
        // 微信:SUCCESS -> 1
        if ("TRADE_SUCCESS".equals(channelStatus) || "SUCCESS".equals(channelStatus)) {
            return localStatus == 1;  // 支付成功
        }
        if ("TRADE_CLOSED".equals(channelStatus) || "CLOSED".equals(channelStatus)) {
            return localStatus == 2;  // 支付失败/关闭
        }
        if ("REFUND".equals(channelStatus)) {
            return localStatus == 3;  // 已退款
        }
        return false;
    }
}

4.4 对账服务

java 复制代码
/**
 * 对账任务服务
 */
@Slf4j
@Service
public class ReconTaskService {

    @Autowired
    private ReconTaskMapper taskMapper;

    @Autowired
    private ReconResultMapper resultMapper;

    @Autowired
    private LocalPaymentRecordMapper localRecordMapper;

    @Autowired
    private List<BillParser> billParsers;

    @Autowired
    private DefaultReconEngine reconEngine;

    @Autowired
    private ChannelBillService channelBillService;

    /**
     * 创建对账任务
     */
    @Transactional
    public ReconTask createTask(String channelType, LocalDate reconDate, String filePath) {
        ReconTask task = new ReconTask();
        task.setTaskNo(generateTaskNo());
        task.setChannelType(channelType);
        task.setReconDate(reconDate);
        task.setFilePath(filePath);
        task.setStatus(TaskStatus.PENDING.getCode());

        taskMapper.insert(task);
        return task;
    }

    /**
     * 执行对账任务
     */
    @Async
    @Transactional
    public void executeTask(Long taskId) {
        ReconTask task = taskMapper.selectById(taskId);
        if (task == null) {
            log.error("对账任务不存在: {}", taskId);
            return;
        }

        try {
            // 更新任务状态
            task.setStatus(TaskStatus.PROCESSING.getCode());
            task.setStartTime(LocalDateTime.now());
            taskMapper.updateById(task);

            // 1. 解析渠道账单
            List<ChannelBillDTO> channelBills = parseBillFile(task);
            task.setTotalCount(channelBills.size());

            // 2. 保存渠道账单
            channelBillService.saveBatch(task.getId(), task.getChannelType(), channelBills);

            // 3. 查询本地支付记录
            List<LocalPaymentRecord> localRecords = queryLocalRecords(
                    task.getChannelType(), task.getReconDate());

            // 4. 执行对账
            List<ReconResult> results = reconEngine.batchReconcile(channelBills, localRecords);

            // 5. 保存对账结果
            saveReconResults(task.getId(), results);

            // 6. 更新任务统计
            updateTaskStatistics(task, results);

            task.setStatus(TaskStatus.COMPLETED.getCode());
            task.setEndTime(LocalDateTime.now());

        } catch (Exception e) {
            log.error("对账任务执行失败: {}", taskId, e);
            task.setStatus(TaskStatus.FAILED.getCode());
            task.setErrorMsg(e.getMessage());
            task.setEndTime(LocalDateTime.now());
        }

        taskMapper.updateById(task);
    }

    /**
     * 解析账单文件
     */
    private List<ChannelBillDTO> parseBillFile(ReconTask task) throws Exception {
        // 获取文件输入流
        InputStream inputStream = getFileInputStream(task.getFilePath());

        // 查找匹配的解析器
        BillParser parser = billParsers.stream()
                .filter(p -> p.getChannelType().getCode().equals(task.getChannelType()))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("不支持的渠道类型: " + task.getChannelType()));

        return parser.parse(inputStream, task.getFilePath());
    }

    /**
     * 查询本地支付记录
     */
    private List<LocalPaymentRecord> queryLocalRecords(String channelType, LocalDate reconDate) {
        LocalDateTime startTime = reconDate.atStartOfDay();
        LocalDateTime endTime = reconDate.plusDays(1).atStartOfDay();

        return localRecordMapper.selectByChannelAndTime(channelType, startTime, endTime);
    }

    /**
     * 保存对账结果
     */
    @Transactional
    public void saveReconResults(Long taskId, List<ReconResult> results) {
        // 批量插入
        List<List<ReconResult>> batches = Lists.partition(results, 500);
        for (List<ReconResult> batch : batches) {
            batch.forEach(r -> r.setTaskId(taskId));
            resultMapper.insertBatch(batch);
        }
    }

    /**
     * 更新任务统计
     */
    private void updateTaskStatistics(ReconTask task, List<ReconResult> results) {
        int successCount = 0;
        int diffCount = 0;

        for (ReconResult result : results) {
            if (result.getResultType() == ReconResultType.MATCHED.getCode()) {
                successCount++;
            } else {
                diffCount++;
            }
        }

        task.setSuccessCount(successCount);
        task.setDiffCount(diffCount);
        task.setFailCount(0);
    }

    private String generateTaskNo() {
        return "RECON" + System.currentTimeMillis() +
                String.format("%04d", new Random().nextInt(10000));
    }

    private InputStream getFileInputStream(String filePath) throws Exception {
        // 支持本地文件和 OSS 文件
        if (filePath.startsWith("http")) {
            return new URL(filePath).openStream();
        }
        return new FileInputStream(filePath);
    }
}

4.5 定时对账任务

java 复制代码
/**
 * 对账定时任务
 */
@Slf4j
@Component
public class ReconScheduleTask {

    @Autowired
    private ReconTaskService taskService;

    @Autowired
    private BillDownloadService billDownloadService;

    /**
     * 每天凌晨 1 点执行前一天的对账
     */
    @Scheduled(cron = "0 0 1 * * ?")
    public void dailyReconciliation() {
        LocalDate reconDate = LocalDate.now().minusDays(1);
        log.info("开始执行 {} 的对账任务", reconDate);

        // 支付宝对账
        executeChannelRecon(ChannelType.ALIPAY, reconDate);

        // 微信对账
        executeChannelRecon(ChannelType.WECHAT, reconDate);

        // 银行对账
        executeChannelRecon(ChannelType.BANK, reconDate);

        log.info("{} 的对账任务创建完成", reconDate);
    }

    private void executeChannelRecon(ChannelType channelType, LocalDate reconDate) {
        try {
            // 1. 下载账单文件
            String filePath = billDownloadService.downloadBill(channelType, reconDate);

            // 2. 创建对账任务
            ReconTask task = taskService.createTask(
                    channelType.getCode(), reconDate, filePath);

            // 3. 异步执行对账
            taskService.executeTask(task.getId());

        } catch (Exception e) {
            log.error("{}渠道对账任务创建失败: {}", channelType.getDesc(), e.getMessage(), e);
        }
    }
}

/**
 * 账单下载服务
 */
@Slf4j
@Service
public class BillDownloadService {

    @Autowired
    private AlipayClient alipayClient;

    @Autowired
    private WxPayService wxPayService;

    /**
     * 下载渠道账单
     */
    public String downloadBill(ChannelType channelType, LocalDate billDate) throws Exception {
        switch (channelType) {
            case ALIPAY:
                return downloadAlipayBill(billDate);
            case WECHAT:
                return downloadWechatBill(billDate);
            case BANK:
                return downloadBankBill(billDate);
            default:
                throw new RuntimeException("不支持的渠道类型: " + channelType);
        }
    }

    /**
     * 下载支付宝账单
     */
    private String downloadAlipayBill(LocalDate billDate) throws Exception {
        AlipayDataDataserviceBillDownloadurlQueryRequest request =
                new AlipayDataDataserviceBillDownloadurlQueryRequest();

        Map<String, String> params = new HashMap<>();
        params.put("bill_type", "trade");
        params.put("bill_date", billDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        request.setBizContent(JSON.toJSONString(params));

        AlipayDataDataserviceBillDownloadurlQueryResponse response =
                alipayClient.execute(request);

        if (response.isSuccess()) {
            String downloadUrl = response.getBillDownloadUrl();
            return downloadAndSaveFile(downloadUrl, "alipay", billDate);
        } else {
            throw new RuntimeException("获取支付宝账单下载地址失败: " + response.getMsg());
        }
    }

    /**
     * 下载微信账单
     */
    private String downloadWechatBill(LocalDate billDate) throws Exception {
        WxPayDownloadBillRequest request = new WxPayDownloadBillRequest();
        request.setBillDate(billDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
        request.setBillType("ALL");

        WxPayDownloadBillResult result = wxPayService.downloadBill(request);

        // 保存账单内容到文件
        String fileName = String.format("wechat_bill_%s.csv",
                billDate.format(DateTimeFormatter.BASIC_ISO_DATE));
        String filePath = "/data/bills/" + fileName;

        Files.write(Paths.get(filePath), result.getBillContent().getBytes(StandardCharsets.UTF_8));

        return filePath;
    }

    /**
     * 下载银行账单(通常是 SFTP 方式)
     */
    private String downloadBankBill(LocalDate billDate) throws Exception {
        // 银行账单通常通过 SFTP 获取
        // 这里简化处理
        String fileName = String.format("bank_bill_%s.xlsx",
                billDate.format(DateTimeFormatter.BASIC_ISO_DATE));
        return "/data/bills/" + fileName;
    }

    private String downloadAndSaveFile(String url, String prefix, LocalDate billDate)
            throws Exception {
        String fileName = String.format("%s_bill_%s.csv",
                prefix, billDate.format(DateTimeFormatter.BASIC_ISO_DATE));
        String filePath = "/data/bills/" + fileName;

        // 下载文件
        try (InputStream in = new URL(url).openStream()) {
            Files.copy(in, Paths.get(filePath), StandardCopyOption.REPLACE_EXISTING);
        }

        return filePath;
    }
}

4.6 差异处理服务

java 复制代码
/**
 * 差异处理服务
 */
@Slf4j
@Service
public class DiffHandleService {

    @Autowired
    private ReconResultMapper resultMapper;

    @Autowired
    private LocalPaymentRecordMapper localRecordMapper;

    /**
     * 查询待处理差异
     */
    public List<ReconResult> queryPendingDiffs(Long taskId) {
        return resultMapper.selectByTaskAndStatus(taskId, 0);
    }

    /**
     * 处理差异:标记为已处理
     */
    @Transactional
    public void handleDiff(Long resultId, String handleRemark, String handler) {
        ReconResult result = resultMapper.selectById(resultId);
        if (result == null) {
            throw new RuntimeException("对账结果不存在");
        }

        result.setHandleStatus(1);  // 已处理
        result.setHandleRemark(handleRemark);
        result.setHandler(handler);
        result.setHandleTime(LocalDateTime.now());

        resultMapper.updateById(result);

        // 更新本地记录的对账状态
        if (result.getOrderNo() != null) {
            LocalPaymentRecord localRecord = localRecordMapper.selectByOrderNo(result.getOrderNo());
            if (localRecord != null) {
                localRecord.setReconStatus(1);  // 已对账
                localRecord.setReconTime(LocalDateTime.now());
                localRecordMapper.updateById(localRecord);
            }
        }
    }

    /**
     * 忽略差异
     */
    @Transactional
    public void ignoreDiff(Long resultId, String reason, String handler) {
        ReconResult result = resultMapper.selectById(resultId);
        if (result == null) {
            throw new RuntimeException("对账结果不存在");
        }

        result.setHandleStatus(2);  // 忽略
        result.setHandleRemark("忽略原因: " + reason);
        result.setHandler(handler);
        result.setHandleTime(LocalDateTime.now());

        resultMapper.updateById(result);
    }

    /**
     * 批量处理短款(渠道有本地无)
     * 自动创建本地补单记录
     */
    @Transactional
    public void batchHandleChannelMore(List<Long> resultIds, String handler) {
        for (Long resultId : resultIds) {
            ReconResult result = resultMapper.selectById(resultId);
            if (result == null || result.getResultType() != ReconResultType.CHANNEL_MORE.getCode()) {
                continue;
            }

            // 创建补单记录
            LocalPaymentRecord record = new LocalPaymentRecord();
            record.setOrderNo(result.getChannelTradeNo());  // 使用渠道交易号作为订单号
            record.setChannelTradeNo(result.getChannelTradeNo());
            record.setAmount(result.getChannelAmount());
            record.setPayStatus(1);  // 支付成功
            record.setReconStatus(1);  // 已对账
            record.setReconTime(LocalDateTime.now());

            localRecordMapper.insert(record);

            // 更新差异处理状态
            result.setHandleStatus(1);
            result.setHandleRemark("自动补单处理");
            result.setHandler(handler);
            result.setHandleTime(LocalDateTime.now());
            resultMapper.updateById(result);

            log.info("短款自动补单: channelTradeNo={}", result.getChannelTradeNo());
        }
    }

    /**
     * 自动处理规则
     */
    @Transactional
    public void autoHandleDiffs(Long taskId) {
        List<ReconResult> diffs = queryPendingDiffs(taskId);

        for (ReconResult diff : diffs) {
            // 规则1:金额差异小于1分,自动忽略
            if (diff.getResultType() == ReconResultType.AMOUNT_MISMATCH.getCode()) {
                if (diff.getDiffAmount() != null &&
                        diff.getDiffAmount().compareTo(new BigDecimal("0.01")) <= 0) {
                    ignoreDiff(diff.getId(), "金额差异小于1分,系统自动忽略", "SYSTEM");
                }
            }

            // 规则2:时间跨天差异,自动忽略(需要额外逻辑判断)
            // ...
        }
    }
}

4.7 对账报告生成

java 复制代码
/**
 * 对账报告服务
 */
@Slf4j
@Service
public class ReconReportService {

    @Autowired
    private ReconTaskMapper taskMapper;

    @Autowired
    private ReconResultMapper resultMapper;

    /**
     * 生成对账报告
     */
    public ReconReport generateReport(Long taskId) {
        ReconTask task = taskMapper.selectById(taskId);
        if (task == null) {
            throw new RuntimeException("对账任务不存在");
        }

        ReconReport report = new ReconReport();
        report.setTaskNo(task.getTaskNo());
        report.setChannelType(task.getChannelType());
        report.setReconDate(task.getReconDate());

        // 统计数据
        report.setTotalCount(task.getTotalCount());
        report.setSuccessCount(task.getSuccessCount());
        report.setDiffCount(task.getDiffCount());

        // 计算对账率
        if (task.getTotalCount() > 0) {
            double successRate = (double) task.getSuccessCount() / task.getTotalCount() * 100;
            report.setSuccessRate(String.format("%.2f%%", successRate));
        }

        // 差异分类统计
        Map<Integer, Long> diffTypeCount = resultMapper.countByResultType(taskId);
        report.setLocalMoreCount(diffTypeCount.getOrDefault(ReconResultType.LOCAL_MORE.getCode(), 0L));
        report.setChannelMoreCount(diffTypeCount.getOrDefault(ReconResultType.CHANNEL_MORE.getCode(), 0L));
        report.setAmountMismatchCount(diffTypeCount.getOrDefault(ReconResultType.AMOUNT_MISMATCH.getCode(), 0L));
        report.setStatusMismatchCount(diffTypeCount.getOrDefault(ReconResultType.STATUS_MISMATCH.getCode(), 0L));

        // 差异金额统计
        BigDecimal totalDiffAmount = resultMapper.sumDiffAmount(taskId);
        report.setTotalDiffAmount(totalDiffAmount);

        // 生成时间
        report.setGenerateTime(LocalDateTime.now());

        return report;
    }

    /**
     * 导出对账报告 Excel
     */
    public void exportReportExcel(Long taskId, OutputStream outputStream) throws Exception {
        ReconReport report = generateReport(taskId);
        List<ReconResult> diffs = resultMapper.selectDiffsByTaskId(taskId);

        // 使用 EasyExcel 导出
        ExcelWriter excelWriter = EasyExcel.write(outputStream).build();

        // Sheet1: 报告摘要
        WriteSheet summarySheet = EasyExcel.writerSheet(0, "对账摘要")
                .head(ReportSummary.class).build();
        excelWriter.write(Collections.singletonList(convertToSummary(report)), summarySheet);

        // Sheet2: 差异明细
        WriteSheet diffSheet = EasyExcel.writerSheet(1, "差异明细")
                .head(DiffDetail.class).build();
        excelWriter.write(convertToDiffDetails(diffs), diffSheet);

        excelWriter.finish();
    }

    @Data
    public static class ReconReport {
        private String taskNo;
        private String channelType;
        private LocalDate reconDate;
        private Integer totalCount;
        private Integer successCount;
        private Integer diffCount;
        private String successRate;
        private Long localMoreCount;
        private Long channelMoreCount;
        private Long amountMismatchCount;
        private Long statusMismatchCount;
        private BigDecimal totalDiffAmount;
        private LocalDateTime generateTime;
    }

    @Data
    public static class ReportSummary {
        @ExcelProperty("任务编号")
        private String taskNo;

        @ExcelProperty("渠道类型")
        private String channelType;

        @ExcelProperty("对账日期")
        private String reconDate;

        @ExcelProperty("总记录数")
        private Integer totalCount;

        @ExcelProperty("成功数")
        private Integer successCount;

        @ExcelProperty("差异数")
        private Integer diffCount;

        @ExcelProperty("对账成功率")
        private String successRate;
    }

    @Data
    public static class DiffDetail {
        @ExcelProperty("商户订单号")
        private String orderNo;

        @ExcelProperty("渠道交易号")
        private String channelTradeNo;

        @ExcelProperty("差异类型")
        private String resultType;

        @ExcelProperty("本地金额")
        private BigDecimal localAmount;

        @ExcelProperty("渠道金额")
        private BigDecimal channelAmount;

        @ExcelProperty("差异金额")
        private BigDecimal diffAmount;

        @ExcelProperty("处理状态")
        private String handleStatus;
    }

    private ReportSummary convertToSummary(ReconReport report) {
        ReportSummary summary = new ReportSummary();
        summary.setTaskNo(report.getTaskNo());
        summary.setChannelType(report.getChannelType());
        summary.setReconDate(report.getReconDate().toString());
        summary.setTotalCount(report.getTotalCount());
        summary.setSuccessCount(report.getSuccessCount());
        summary.setDiffCount(report.getDiffCount());
        summary.setSuccessRate(report.getSuccessRate());
        return summary;
    }

    private List<DiffDetail> convertToDiffDetails(List<ReconResult> diffs) {
        return diffs.stream().map(d -> {
            DiffDetail detail = new DiffDetail();
            detail.setOrderNo(d.getOrderNo());
            detail.setChannelTradeNo(d.getChannelTradeNo());
            detail.setResultType(ReconResultType.values()[d.getResultType() - 1].getDesc());
            detail.setLocalAmount(d.getLocalAmount());
            detail.setChannelAmount(d.getChannelAmount());
            detail.setDiffAmount(d.getDiffAmount());
            detail.setHandleStatus(d.getHandleStatus() == 0 ? "待处理" :
                    d.getHandleStatus() == 1 ? "已处理" : "已忽略");
            return detail;
        }).collect(Collectors.toList());
    }
}

五、自定义渠道导入

5.1 自定义渠道配置

java 复制代码
/**
 * 渠道配置实体
 */
@Data
@TableName("recon_channel_config")
public class ReconChannelConfig {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String channelCode;
    private String channelName;
    private String fileType;
    private String fileEncoding;
    private String fieldMapping;  // JSON 格式
    private String parserClass;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

/**
 * 渠道配置服务
 */
@Slf4j
@Service
public class ChannelConfigService {

    @Autowired
    private ReconChannelConfigMapper configMapper;

    /**
     * 创建自定义渠道配置
     */
    public void createChannelConfig(ChannelConfigDTO dto) {
        // 验证字段映射
        validateFieldMapping(dto.getFieldMapping());

        ReconChannelConfig config = new ReconChannelConfig();
        config.setChannelCode(dto.getChannelCode());
        config.setChannelName(dto.getChannelName());
        config.setFileType(dto.getFileType());
        config.setFileEncoding(dto.getFileEncoding());
        config.setFieldMapping(JSON.toJSONString(dto.getFieldMapping()));
        config.setStatus(1);

        configMapper.insert(config);
    }

    /**
     * 验证字段映射配置
     */
    private void validateFieldMapping(Map<String, String> fieldMapping) {
        // 必须包含的字段
        List<String> requiredFields = Arrays.asList(
                "channelTradeNo", "merchantOrderNo", "amount", "tradeStatus"
        );

        for (String field : requiredFields) {
            if (!fieldMapping.containsKey(field)) {
                throw new RuntimeException("缺少必要字段映射: " + field);
            }
        }
    }

    @Data
    public static class ChannelConfigDTO {
        private String channelCode;
        private String channelName;
        private String fileType;  // CSV / EXCEL
        private String fileEncoding;
        private Map<String, String> fieldMapping;
    }
}

5.2 文件上传与导入

java 复制代码
/**
 * 文件上传控制器
 */
@Slf4j
@RestController
@RequestMapping("/api/recon")
public class ReconController {

    @Autowired
    private ReconTaskService taskService;

    @Autowired
    private CustomBillParserFactory parserFactory;

    @Autowired
    private ChannelBillService channelBillService;

    /**
     * 上传自定义渠道账单
     */
    @PostMapping("/upload/custom")
    public Result<?> uploadCustomBill(
            @RequestParam("file") MultipartFile file,
            @RequestParam("channelCode") String channelCode,
            @RequestParam("reconDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate reconDate) {

        try {
            // 1. 保存文件
            String filePath = saveUploadFile(file);

            // 2. 创建对账任务
            ReconTask task = taskService.createTask("CUSTOM", reconDate, filePath);

            // 3. 解析账单
            List<ChannelBillDTO> bills = parserFactory.parse(channelCode, file.getInputStream());

            // 4. 保存账单数据
            channelBillService.saveCustomBills(task.getId(), channelCode, bills);

            // 5. 异步执行对账
            taskService.executeTask(task.getId());

            return Result.success(task.getTaskNo());

        } catch (Exception e) {
            log.error("上传自定义渠道账单失败", e);
            return Result.fail("上传失败: " + e.getMessage());
        }
    }

    /**
     * 上传支付宝/微信账单
     */
    @PostMapping("/upload/{channelType}")
    public Result<?> uploadBill(
            @PathVariable String channelType,
            @RequestParam("file") MultipartFile file,
            @RequestParam("reconDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate reconDate) {

        try {
            // 保存文件
            String filePath = saveUploadFile(file);

            // 创建并执行对账任务
            ReconTask task = taskService.createTask(channelType.toUpperCase(), reconDate, filePath);
            taskService.executeTask(task.getId());

            return Result.success(task.getTaskNo());

        } catch (Exception e) {
            log.error("上传账单失败", e);
            return Result.fail("上传失败: " + e.getMessage());
        }
    }

    /**
     * 查询对账任务状态
     */
    @GetMapping("/task/{taskNo}")
    public Result<ReconTask> getTaskStatus(@PathVariable String taskNo) {
        ReconTask task = taskService.getByTaskNo(taskNo);
        return Result.success(task);
    }

    /**
     * 查询对账差异列表
     */
    @GetMapping("/diff/list")
    public Result<PageInfo<ReconResult>> getDiffList(
            @RequestParam Long taskId,
            @RequestParam(defaultValue = "1") int pageNum,
            @RequestParam(defaultValue = "20") int pageSize) {

        PageHelper.startPage(pageNum, pageSize);
        List<ReconResult> diffs = taskService.getDiffList(taskId);

        return Result.success(new PageInfo<>(diffs));
    }

    /**
     * 导出对账报告
     */
    @GetMapping("/report/export")
    public void exportReport(@RequestParam Long taskId, HttpServletResponse response)
            throws Exception {

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition",
                "attachment;filename=recon_report_" + taskId + ".xlsx");

        taskService.exportReport(taskId, response.getOutputStream());
    }

    private String saveUploadFile(MultipartFile file) throws Exception {
        String originalFilename = file.getOriginalFilename();
        String fileName = System.currentTimeMillis() + "_" + originalFilename;
        String filePath = "/data/bills/upload/" + fileName;

        File destFile = new File(filePath);
        if (!destFile.getParentFile().exists()) {
            destFile.getParentFile().mkdirs();
        }

        file.transferTo(destFile);
        return filePath;
    }
}

六、监控告警

6.1 对账监控指标

java 复制代码
/**
 * 对账监控服务
 */
@Slf4j
@Service
public class ReconMonitorService {

    @Autowired
    private ReconTaskMapper taskMapper;

    @Autowired
    private ReconResultMapper resultMapper;

    @Autowired
    private AlertService alertService;

    /**
     * 监控对账任务执行情况
     */
    @Scheduled(cron = "0 0/30 * * * ?")
    public void monitorTaskExecution() {
        // 检查超时任务(处理中超过1小时)
        List<ReconTask> timeoutTasks = taskMapper.selectTimeoutTasks(
                TaskStatus.PROCESSING.getCode(), 60);

        for (ReconTask task : timeoutTasks) {
            log.warn("对账任务执行超时: taskNo={}", task.getTaskNo());
            alertService.sendAlert(AlertLevel.WARNING,
                    "对账任务执行超时", "任务编号: " + task.getTaskNo());
        }

        // 检查失败任务
        List<ReconTask> failedTasks = taskMapper.selectTodayFailedTasks();
        if (!failedTasks.isEmpty()) {
            log.error("今日存在失败的对账任务: {}", failedTasks.size());
            alertService.sendAlert(AlertLevel.ERROR,
                    "对账任务执行失败", "失败任务数: " + failedTasks.size());
        }
    }

    /**
     * 监控差异率
     */
    @Scheduled(cron = "0 0 9 * * ?")
    public void monitorDiffRate() {
        LocalDate yesterday = LocalDate.now().minusDays(1);

        List<ReconTask> tasks = taskMapper.selectByReconDate(yesterday);

        for (ReconTask task : tasks) {
            if (task.getTotalCount() == null || task.getTotalCount() == 0) {
                continue;
            }

            // 计算差异率
            double diffRate = (double) task.getDiffCount() / task.getTotalCount() * 100;

            // 差异率超过阈值告警
            if (diffRate > 1.0) {  // 差异率超过1%
                log.warn("对账差异率异常: channel={}, date={}, diffRate={}%",
                        task.getChannelType(), task.getReconDate(), diffRate);

                alertService.sendAlert(AlertLevel.WARNING,
                        "对账差异率异常",
                        String.format("渠道: %s, 日期: %s, 差异率: %.2f%%",
                                task.getChannelType(), task.getReconDate(), diffRate));
            }
        }
    }

    /**
     * 获取对账看板数据
     */
    public DashboardData getDashboardData(LocalDate startDate, LocalDate endDate) {
        DashboardData data = new DashboardData();

        // 任务统计
        data.setTotalTasks(taskMapper.countByDateRange(startDate, endDate));
        data.setSuccessTasks(taskMapper.countByStatusAndDateRange(
                TaskStatus.COMPLETED.getCode(), startDate, endDate));
        data.setFailedTasks(taskMapper.countByStatusAndDateRange(
                TaskStatus.FAILED.getCode(), startDate, endDate));

        // 差异统计
        data.setTotalDiffs(resultMapper.countDiffsByDateRange(startDate, endDate));
        data.setHandledDiffs(resultMapper.countHandledDiffsByDateRange(startDate, endDate));
        data.setPendingDiffs(data.getTotalDiffs() - data.getHandledDiffs());

        // 差异金额
        data.setTotalDiffAmount(resultMapper.sumDiffAmountByDateRange(startDate, endDate));

        // 渠道统计
        data.setChannelStats(taskMapper.groupByChannel(startDate, endDate));

        // 趋势数据
        data.setTrendData(taskMapper.getDailyTrend(startDate, endDate));

        return data;
    }

    @Data
    public static class DashboardData {
        private Long totalTasks;
        private Long successTasks;
        private Long failedTasks;
        private Long totalDiffs;
        private Long handledDiffs;
        private Long pendingDiffs;
        private BigDecimal totalDiffAmount;
        private List<ChannelStat> channelStats;
        private List<TrendData> trendData;
    }

    @Data
    public static class ChannelStat {
        private String channelType;
        private Long taskCount;
        private Long diffCount;
        private Double diffRate;
    }

    @Data
    public static class TrendData {
        private LocalDate date;
        private Long totalCount;
        private Long successCount;
        private Long diffCount;
    }
}

6.2 告警服务

java 复制代码
/**
 * 告警级别
 */
public enum AlertLevel {
    INFO, WARNING, ERROR, CRITICAL
}

/**
 * 告警服务
 */
@Slf4j
@Service
public class AlertService {

    @Value("${alert.dingtalk.webhook}")
    private String dingTalkWebhook;

    @Value("${alert.email.to}")
    private String emailTo;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private JavaMailSender mailSender;

    /**
     * 发送告警
     */
    public void sendAlert(AlertLevel level, String title, String content) {
        log.info("发送告警: level={}, title={}", level, title);

        // 根据告警级别选择通知方式
        switch (level) {
            case INFO:
                // 仅记录日志
                break;
            case WARNING:
                sendDingTalkAlert(title, content);
                break;
            case ERROR:
            case CRITICAL:
                sendDingTalkAlert(title, content);
                sendEmailAlert(title, content);
                break;
        }
    }

    /**
     * 发送钉钉告警
     */
    private void sendDingTalkAlert(String title, String content) {
        try {
            Map<String, Object> message = new HashMap<>();
            message.put("msgtype", "markdown");

            Map<String, String> markdown = new HashMap<>();
            markdown.put("title", title);
            markdown.put("text", String.format("### %s\n\n%s\n\n> 时间: %s",
                    title, content, LocalDateTime.now().format(
                            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))));

            message.put("markdown", markdown);

            restTemplate.postForObject(dingTalkWebhook, message, String.class);

        } catch (Exception e) {
            log.error("发送钉钉告警失败", e);
        }
    }

    /**
     * 发送邮件告警
     */
    private void sendEmailAlert(String title, String content) {
        try {
            SimpleMailMessage message = new SimpleMailMessage();
            message.setTo(emailTo.split(","));
            message.setSubject("[对账系统告警] " + title);
            message.setText(content + "\n\n时间: " + LocalDateTime.now());

            mailSender.send(message);

        } catch (Exception e) {
            log.error("发送邮件告警失败", e);
        }
    }
}

七、系统优化

7.1 大文件处理优化

java 复制代码
/**
 * 分片解析大文件
 */
@Slf4j
@Component
public class LargeFileBillParser {

    private static final int BATCH_SIZE = 10000;

    @Autowired
    private ChannelBillService channelBillService;

    /**
     * 流式解析大文件
     */
    public void parseAndSave(Long taskId, String channelType,
                              InputStream inputStream) throws Exception {

        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {

            String line;
            List<ChannelBillDTO> batch = new ArrayList<>(BATCH_SIZE);
            int totalCount = 0;

            // 跳过表头
            reader.readLine();

            while ((line = reader.readLine()) != null) {
                if (line.trim().isEmpty()) continue;

                ChannelBillDTO bill = parseLine(line);
                if (bill != null) {
                    batch.add(bill);
                    totalCount++;
                }

                // 批量保存
                if (batch.size() >= BATCH_SIZE) {
                    channelBillService.saveBatch(taskId, channelType, batch);
                    log.info("已处理 {} 条记录", totalCount);
                    batch.clear();
                }
            }

            // 保存剩余数据
            if (!batch.isEmpty()) {
                channelBillService.saveBatch(taskId, channelType, batch);
            }

            log.info("文件解析完成,共 {} 条记录", totalCount);
        }
    }

    private ChannelBillDTO parseLine(String line) {
        // 解析逻辑
        return null;
    }
}

7.2 并行对账优化

java 复制代码
/**
 * 并行对账引擎
 */
@Slf4j
@Service
public class ParallelReconEngine {

    @Autowired
    private DefaultReconEngine reconEngine;

    private final ExecutorService executor = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors());

    /**
     * 并行批量对账
     */
    public List<ReconResult> parallelReconcile(List<ChannelBillDTO> channelBills,
                                                 List<LocalPaymentRecord> localRecords) {

        // 构建本地记录索引
        Map<String, LocalPaymentRecord> localRecordMap = localRecords.stream()
                .collect(Collectors.toMap(
                        LocalPaymentRecord::getOrderNo,
                        r -> r,
                        (r1, r2) -> r1
                ));

        // 分片并行处理
        int partitionSize = 1000;
        List<List<ChannelBillDTO>> partitions = Lists.partition(channelBills, partitionSize);

        List<CompletableFuture<List<ReconResult>>> futures = partitions.stream()
                .map(partition -> CompletableFuture.supplyAsync(() ->
                        processPartition(partition, localRecordMap), executor))
                .collect(Collectors.toList());

        // 合并结果
        return futures.stream()
                .map(CompletableFuture::join)
                .flatMap(List::stream)
                .collect(Collectors.toList());
    }

    private List<ReconResult> processPartition(List<ChannelBillDTO> partition,
                                                 Map<String, LocalPaymentRecord> localRecordMap) {
        List<ReconResult> results = new ArrayList<>();

        for (ChannelBillDTO channelBill : partition) {
            LocalPaymentRecord localRecord = localRecordMap.get(channelBill.getMerchantOrderNo());
            ReconResult result = reconEngine.reconcile(channelBill, localRecord);
            results.add(result);
        }

        return results;
    }
}

7.3 缓存优化

java 复制代码
/**
 * 对账缓存服务
 */
@Slf4j
@Service
public class ReconCacheService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String TASK_KEY_PREFIX = "recon:task:";
    private static final String RESULT_KEY_PREFIX = "recon:result:";

    /**
     * 缓存任务信息
     */
    public void cacheTask(ReconTask task) {
        String key = TASK_KEY_PREFIX + task.getTaskNo();
        redisTemplate.opsForValue().set(key, task, Duration.ofHours(24));
    }

    /**
     * 获取缓存的任务
     */
    public ReconTask getTaskFromCache(String taskNo) {
        String key = TASK_KEY_PREFIX + taskNo;
        return (ReconTask) redisTemplate.opsForValue().get(key);
    }

    /**
     * 缓存对账进度
     */
    public void updateProgress(String taskNo, int processedCount, int totalCount) {
        String key = TASK_KEY_PREFIX + taskNo + ":progress";
        Map<String, Integer> progress = new HashMap<>();
        progress.put("processed", processedCount);
        progress.put("total", totalCount);
        redisTemplate.opsForValue().set(key, progress, Duration.ofHours(2));
    }

    /**
     * 获取对账进度
     */
    @SuppressWarnings("unchecked")
    public Map<String, Integer> getProgress(String taskNo) {
        String key = TASK_KEY_PREFIX + taskNo + ":progress";
        return (Map<String, Integer>) redisTemplate.opsForValue().get(key);
    }
}

八、最佳实践与总结

8.1 对账系统设计要点

diff 复制代码
+------------------------------------------------------------------+
|                    对账系统设计要点                                |
+------------------------------------------------------------------+
|                                                                   |
|   1. 可扩展性                                                      |
|   - 支持多渠道接入                                                 |
|   - 解析器插件化                                                  |
|   - 字段映射可配置                                                 |
|                                                                   |
|   2. 可靠性                                                        |
|   - 任务失败重试                                                  |
|   - 断点续传                                                      |
|   - 数据校验                                                      |
|                                                                   |
|   3. 性能                                                         |
|   - 大文件分片处理                                                 |
|   - 批量数据库操作                                                 |
|   - 并行对账                                                      |
|                                                                   |
|   4. 可观测性                                                      |
|   - 任务状态监控                                                  |
|   - 差异率告警                                                    |
|   - 对账报告                                                      |
|                                                                   |
+------------------------------------------------------------------+

8.2 常见问题处理

问题类型 原因分析 解决方案
时间差异 跨天交易、时区问题 扩大时间范围查询,统一时区
金额差异 手续费计算方式不同 明确金额口径,分开比对
订单号不匹配 格式不一致、前缀差异 标准化订单号格式
重复对账 同一笔交易多次出现 去重处理,幂等设计
账单延迟 渠道账单生成延迟 延后对账时间,增加容错

8.3 运维建议

  1. 对账时间选择:建议凌晨执行,避免影响业务
  2. 账单保留策略:至少保留 3 个月,便于追溯
  3. 差异处理 SLA:制定差异处理时效要求
  4. 定期巡检:每周检查对账任务执行情况
  5. 容灾备份:对账数据定期备份

相关推荐
0***h94240 分钟前
使用 java -jar 命令启动 Spring Boot 应用时,指定特定的配置文件的几种实现方式
java·spring boot·jar
雨中飘荡的记忆43 分钟前
布式事务详解:从理论到实践(RocketMQ + Seata)
java·rocketmq
i***48611 小时前
微服务生态组件之Spring Cloud LoadBalancer详解和源码分析
java·spring cloud·微服务
zzlyx991 小时前
用C#采用Avalonia+Mapsui在离线地图上插入图片画信号扩散图
java·开发语言·c#
Aevget1 小时前
MyEclipse全新发布v2025.2——AI + Java 24 +更快的调试
java·ide·人工智能·eclipse·myeclipse
一 乐1 小时前
购物|明星周边商城|基于springboot的明星周边商城系统设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·spring
笃行客从不躺平1 小时前
线程池监控是什么
java·开发语言
y1y1z1 小时前
Spring框架教程
java·后端·spring
曾经的三心草2 小时前
基于正倒排索引的Java文档搜索引擎3-实现Index类-实现搜索模块-实现DocSearcher类
java·python·搜索引擎