Skip to content

工商金融业务(CMB)技术详解

对公业务系统的核心技术实现


一、业务概述

工商金融业务(Commercial Banking,简称CMB)是汇丰面向企业客户的业务线,与零售银行业务(RBWM)相比,对公业务在交易金额、操作复杂度、合规要求等方面都有显著不同。作为开发人员,理解CMB的技术特点对于开发企业银行系统至关重要。

1.1 业务线定位

CMB主要服务以下客户群体:

  • 中小企业
  • 大型企业集团
  • 跨国公司
  • 小微企业主

1.2 产品体系

CMB的核心产品线包括:

产品类别典型产品技术复杂度
账户服务企业账户、多级授权、批量操作
现金管理代收代付、资金归集、现金池
贸易金融信用证、保函、票据、福费廷很高
企业贷款流动资金贷款、项目贷款、供应链金融

二、核心业务功能

2.1 企业账户管理

2.1.1 多授权人体系

企业账户与个人账户最大的区别在于可能存在多个授权操作人,每个授权人可能有不同的操作权限。

java
@Entity
@Table(name = "corporate_account")
public class CorporateAccount {

    @Id
    @Column(name = "account_id")
    private String accountId;

    @Column(name = "company_id")
    private String companyId;           // 所属企业

    @Column(name = "account_number")
    private String accountNumber;       // 账号

    @Column(name = "account_type")
    private AccountType accountType;    // 基本户/一般户/专户

    @Column(name = "balance")
    private BigDecimal balance;

    @Column(name = "status")
    private AccountStatus status;

    // 多授权人关系
    @OneToMany(mappedBy = "account")
    private List<AccountSigner> signers;
}

@Entity
@Table(name = "account_signer")
public class AccountSigner {

    @Id
    @Column(name = "signer_id")
    private String signerId;

    @Column(name = "account_id")
    private String accountId;

    @Column(name = "customer_id")
    private String customerId;           // 授权人客户号

    @Column(name = "signer_type")
    private SignerType signerType;      // 单独授权/联合授权

    @Column(name = "daily_limit")
    private BigDecimal dailyLimit;      // 日累计限额

    @Column(name = "single_limit")
    private BigDecimal singleLimit;     // 单笔限额

    @Column(name = "authority_level")
    private Integer authorityLevel;     // 授权层级(1-5)
}

2.1.2 多级授权机制

企业转账通常需要多级审批,授权规则可以配置:

java
/**
 * 多级授权服务
 */
@Service
public class MultiLevelAuthorizationService {

    /**
     * 检查交易是否需要授权
     */
    public AuthorizationCheckResult checkAuthorization(
            String accountId,
            BigDecimal amount,
            String operatorId) {

        // 1. 获取账户的授权规则
        AuthorizationRule rule = authorizationRuleRepository
            .findByAccountId(accountId)
            .orElse(getDefaultRule());

        // 2. 获取操作人的权限
        Signer signer = signerRepository
            .findByAccountIdAndCustomerId(accountId, operatorId)
            .orElseThrow(() -> new UnauthorizedException());

        // 3. 判断是否需要授权
        if (amount.compareTo(signer.getSingleLimit()) <= 0) {
            // 单笔限额内,无需授权
            return AuthorizationCheckResult.noAuthRequired();
        }

        if (amount.compareTo(signer.getDailyLimit()) <= 0) {
            // 日累计限额内,检查是否超过当日累计
            BigDecimal todayAmount = todayTransactionRepository
                .sumAmountByAccountId(accountId);

            if (todayAmount.add(amount).compareTo(signer.getDailyLimit()) <= 0) {
                return AuthorizationCheckResult.noAuthRequired();
            }
        }

        // 4. 需要更高层级授权
        int requiredLevel = calculateRequiredLevel(rule, amount);
        return AuthorizationCheckResult.authRequired(requiredLevel);
    }

    /**
     * 执行授权
     */
    @Transactional
    public AuthorizationResult authorize(
            String transactionId,
            String authorizerId,
            String authCode) {

        // 1. 验证授权码
        if (!authService.verifyAuthCode(authorizerId, authCode)) {
            throw new InvalidAuthCodeException();
        }

        // 2. 更新交易授权状态
        Transaction transaction = transactionRepository.findById(transactionId)
            .orElseThrow(() -> new TransactionNotFoundException());

        transaction.setAuthorizationStatus(AuthorizationStatus.APPROVED);
        transaction.setAuthorizerId(authorizerId);
        transaction.setAuthorizedTime(LocalDateTime.now());

        transactionRepository.save(transaction);

        return AuthorizationResult.success();
    }
}

2.1.3 批量操作

企业客户经常需要批量处理业务,如批量转账、批量代发工资等:

java
/**
 * 批量转账服务
 */
@Service
public class BatchTransferService {

    /**
     * 批量转账
     */
    @Transactional
    public BatchTransferResult batchTransfer(BatchTransferRequest request) {
        List<TransferDetail> details = request.getDetails();
        List<TransferResult> successList = new ArrayList<>();
        List<TransferResult> failList = new ArrayList<>();

        for (TransferDetail detail : details) {
            try {
                // 逐笔处理,记录成功/失败
                String result = singleTransferService.transfer(
                    request.getFromAccountId(),
                    detail.getToAccountNumber(),
                    detail.getAmount(),
                    detail.getRemark()
                );
                successList.add(TransferResult.success(detail, result));
            } catch (Exception e) {
                failList.add(TransferResult.failure(detail, e.getMessage()));
            }
        }

        // 生成批次结果
        BatchTransferResult result = BatchTransferResult.builder()
            .batchId(generateBatchId())
            .totalCount(details.size())
            .successCount(successList.size())
            .failCount(failList.size())
            .successDetails(successList)
            .failDetails(failList)
            .build();

        batchTransferRepository.save(result);
        return result;
    }

    /**
     * 批量转账文件处理
     * 支持CSV/Excel格式
     */
    public BatchTransferResult processBatchFile(
            String accountId,
            MultipartFile file) {

        // 1. 解析文件
        List<TransferDetail> details = fileParser.parse(file);

        // 2. 校验格式
        validationService.validateBatchDetails(details);

        // 3. 汇总金额
        BigDecimal totalAmount = details.stream()
            .map(TransferDetail::getAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);

        // 4. 检查账户余额
        Account account = accountRepository.findById(accountId)
            .orElseThrow();

        if (account.getAvailableBalance().compareTo(totalAmount) < 0) {
            throw new InsufficientBalanceException();
        }

        // 5. 执行批量转账
        return batchTransfer(BatchTransferRequest.builder()
            .fromAccountId(accountId)
            .details(details)
            .build());
    }
}

2.2 现金管理

现金管理是CMB的核心业务,帮助企业客户高效管理资金流动。

2.2.1 代收代付

java
/**
 * 代收服务
 */
@Service
public class CollectionService {

    /**
     * 创建代收指令
     */
    public CollectionInstruction createInstruction(CollectionRequest request) {
        // 1. 校验客户签约状态
        CustomerAgreement agreement = agreementRepository
            .findByCustomerIdAndAgreementType(
                request.getCustomerId(),
                AgreementType.COLLECTION)
            .orElseThrow(() -> new NotSignedException());

        // 2. 校验代收额度
        if (request.getAmount().compareTo(agreement.getSingleLimit()) > 0) {
            throw new AmountExceedLimitException();
        }

        // 3. 创建代收指令
        CollectionInstruction instruction = CollectionInstruction.builder()
            .instructionId(generateInstructionId())
            .customerId(request.getCustomerId())
            .payerAccountNumber(request.getPayerAccountNumber())
            .payerBankName(request.getPayerBankName())
            .amount(request.getAmount())
            .remark(request.getRemark())
            .status(CollectionStatus.PENDING)
            .build();

        instructionRepository.save(instruction);

        // 4. 发送清算指令
        clearingService.sendCollectionInstruction(instruction);

        return instruction;
    }

    /**
     * 代收结果回调
     */
    @Transactional
    public void handleCollectionCallback(CollectionCallback callback) {
        CollectionInstruction instruction = instructionRepository
            .findByInstructionId(callback.getInstructionId())
            .orElseThrow(() -> new InstructionNotFoundException());

        if (callback.isSuccess()) {
            // 成功:资金入账
            instruction.setStatus(CollectionStatus.SUCCESS);
            instruction.setSettleTime(LocalDateTime.now());

            // 资金入企业账户
            accountService.credit(
                instruction.getCustomerId(),
                instruction.getAmount(),
                "代收-" + instruction.getInstructionId()
            );
        } else {
            // 失败:更新状态
            instruction.setStatus(CollectionStatus.FAILED);
            instruction.setFailReason(callback.getFailReason());
        }

        instructionRepository.save(instruction);

        // 5. 发送通知
        notificationService.sendCollectionResult(instruction);
    }
}

/**
 * 代发服务(代发工资等)
 */
@Service
public class PaymentService {

    /**
     * 代发工资
     */
    public PaymentResult batchPayroll(BatchPayrollRequest request) {
        // 1. 校验企业账户
        Account fromAccount = accountRepository.findById(request.getAccountId())
            .orElseThrow();

        // 2. 校验总额
        BigDecimal totalAmount = request.getDetails().stream()
            .map(EmployeePayroll::getAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);

        if (fromAccount.getAvailableBalance().compareTo(totalAmount) < 0) {
            throw new InsufficientBalanceException();
        }

        // 3. 逐笔执行
        List<PayrollResult> results = new ArrayList<>();
        for (EmployeePayroll payroll : request.getDetails()) {
            try {
                executeSinglePayment(fromAccount, payroll);
                results.add(PayrollResult.success(payroll));
            } catch (Exception e) {
                results.add(PayrollResult.failure(payroll, e.getMessage()));
            }
        }

        return PaymentResult.builder()
            .batchId(generateBatchId())
            .results(results)
            .build();
    }
}

2.2.2 资金归集

资金归集是集团客户常用的功能,将子公司账户的资金自动归集到母公司账户:

java
/**
 * 资金归集服务
 */
@Service
public class CashConcentrationService {

    /**
     * 定时执行资金归集
     */
    @Scheduled(cron = "0 0 22 * * ?")  // 每天22:00执行
    @Transactional
    public void executeCashConcentration() {
        // 1. 获取所有生效的归集规则
        List<CashConcentrationRule> rules = ruleRepository
            .findByStatus(RuleStatus.ACTIVE);

        for (CashConcentrationRule rule : rules) {
            try {
                executeConcentration(rule);
            } catch (Exception e) {
                log.error("资金归集失败 ruleId={}", rule.getId(), e);
                alertService.sendAlert("资金归集失败", rule.getId(), e.getMessage());
            }
        }
    }

    /**
     * 执行单笔归集
     */
    @Transactional
    public void executeConcentration(CashConcentrationRule rule) {
        // 1. 获取子账户
        Account subAccount = accountRepository.findById(rule.getSubAccountId())
            .orElseThrow();

        // 2. 计算归集金额
        BigDecimal concentrationAmount = calculateConcentrationAmount(rule, subAccount);

        if (concentrationAmount.compareTo(BigDecimal.ZERO) <= 0) {
            log.info("无需归集,账户余额不足");
            return;
        }

        // 3. 执行转账
        transferService.transfer(
            rule.getSubAccountId(),
            rule.getMainAccountId(),
            concentrationAmount,
            "资金归集"
        );

        // 4. 记录归集日志
        CashConcentrationLog log = CashConcentrationLog.builder()
            .ruleId(rule.getId())
            .subAccountId(rule.getSubAccountId())
            .mainAccountId(rule.getMainAccountId())
            .amount(concentrationAmount)
            .executeTime(LocalDateTime.now())
            .build();

        logRepository.save(log);
    }

    /**
     * 计算归集金额
     */
    private BigDecimal calculateConcentrationAmount(
            CashConcentrationRule rule,
            Account subAccount) {

        BigDecimal balance = subAccount.getAvailableBalance();

        switch (rule.getConcentrationType()) {
            case FULL:
                // 全额归集
                return balance;

            case FIXED:
                // 固定金额归集
                return rule.getFixedAmount();

            case BALANCE:
                // 余额归集(保留固定余额)
                return balance.subtract(rule.getRetainAmount())
                    .max(BigDecimal.ZERO);

            default:
                throw new IllegalArgumentException("未知的归集类型");
        }
    }
}

2.2.3 现金池

现金池是更复杂的资金管理工具,支持:

  • 零余额账户(ZBA):自动将子账户余额归集到主账户
  • 虚拟现金池:只记账不平账
  • 透支共享:集团内账户透支额度共享
java
/**
 * 现金池服务
 */
@Service
public class CashPoolService {

    /**
     * 零余额账户(ZBA)处理
     */
    @Transactional
    public void processZeroBalanceAccount(CashPoolAccount account) {
        BigDecimal balance = account.getBalance();

        if (balance.compareTo(BigDecimal.ZERO) == 0) {
            return;
        }

        // 获取现金池主账户
        CashPool cashPool = cashPoolRepository.findById(account.getCashPoolId())
            .orElseThrow();

        Account mainAccount = accountRepository.findById(cashPool.getMainAccountId())
            .orElseThrow();

        // 执行归集
        if (balance.compareTo(BigDecimal.ZERO) > 0) {
            // 正余额:归集到主账户
            transferService.transfer(
                account.getAccountId(),
                mainAccount.getAccountId(),
                balance,
                "ZBA归集"
            );
        } else {
            // 负余额(透支):从主账户划拨
            transferService.transfer(
                mainAccount.getAccountId(),
                account.getAccountId(),
                balance.abs(),
                "ZBA补足"
            );
        }

        // 记录现金池交易
        cashPoolJournalRepository.save(CashPoolJournal.builder()
            .cashPoolId(cashPool.getId())
            .accountId(account.getAccountId())
            .amount(balance)
            .type(balance.compareTo(BigDecimal.ZERO) > 0 ?
                JournalType.CONCENTRATE : JournalType.SUPPLEMENT)
            .build());
    }
}

2.3 贸易金融

贸易金融是CMB中最复杂的业务领域,涉及信用证、保函、票据等多种金融工具。

2.3.1 信用证业务

信用证(Letter of Credit,LC)是银行根据进口商请求,向出口商开具的一种银行信用保证。

java
/**
 * 信用证服务
 */
@Service
public class LetterOfCreditService {

    /**
     * 开立信用证
     */
    @Transactional
    public LCOpenResult openLC(OpenLCRequest request) {
        // 1. 校验开证申请人资质
        Customer customer = customerRepository.findById(request.getApplicantId())
            .orElseThrow();

        if (!customer.hasLCPermission()) {
            throw new PermissionDeniedException("客户无开证资质");
        }

        // 2. 校验开证额度
        BigDecimal availableLimit = creditLimitService.getAvailableLimit(
            customer.getId(), LimitType.LC);

        if (availableLimit.compareTo(request.getAmount()) < 0) {
            throw new InsufficientLimitException("开证额度不足");
        }

        // 3. 冻结开证额度
        creditLimitService.freezeLimit(
            customer.getId(),
            LimitType.LC,
            request.getAmount(),
            "LC-" + request.getLcNumber()
        );

        // 4. 创建信用证
        LetterOfCredit lc = LetterOfCredit.builder()
            .lcNumber(request.getLcNumber())
            .applicantId(request.getApplicantId())
            .beneficiaryId(request.getBeneficiaryId())
            .amount(request.getAmount())
            .currency(request.getCurrency())
            .issueDate(LocalDate.now())
            .expiryDate(request.getExpiryDate())
            .latestShipDate(request.getLatestShipDate())
            .status(LCStatus.OPENED)
            .lcType(request.getLcType())  // 即期/远期
            .build();

        lcRepository.save(lc);

        // 5. 通知受益人(通过SWIFT或其他渠道)
        swiftService.sendLC(lc);

        return LCOpenResult.success(lc);
    }

    /**
     * 信用证议付(出口商交单)
     */
    @Transactional
    public LCNegotiationResult negotiate(
            String lcNumber,
            List<ShippingDocument> documents) {

        LetterOfCredit lc = lcRepository.findByLcNumber(lcNumber)
            .orElseThrow();

        // 1. 校验单据
        DocumentCheckResult checkResult = documentValidator.check(
            lc, documents);

        if (!checkResult.isPass()) {
            // 单据不符
            return LCNegotiationResult.discrepancy(checkResult.getDiscrepancies());
        }

        // 2. 扣除额度
        creditLimitService.deductLimit(
            lc.getApplicantId(),
            LimitType.LC,
            lc.getAmount()
        );

        // 3. 释放冻结额度
        creditLimitService.unfreezeLimit(
            lc.getApplicantId(),
            LimitType.LC,
            lc.getAmount()
        );

        // 4. 付款
        if (lc.getLcType() == LCTYPE.SIGHT) {
            // 即期信用证:立即付款
            paymentService.payToBeneficiary(
                lc.getBeneficiaryId(),
                lc.getAmount(),
                "LC议付-" + lcNumber
            );
        } else {
            // 远期信用证:到期付款
            // 创建远期付款指令
            createAcceptance(lc);
        }

        // 5. 更新状态
        lc.setStatus(LCStatus.NEGOTIATED);
        lc.setNegotiationDate(LocalDateTime.now());
        lcRepository.save(lc);

        return LCNegotiationResult.success();
    }
}

2.3.2 保函业务

保函(Guarantee)是银行向受益人开具的,保证申请人履行某项义务的书面凭证。

java
/**
 * 保函服务
 */
@Service
public class GuaranteeService {

    /**
     * 开立保函
     */
    @Transactional
    public GuaranteeIssueResult issueGuarantee(IssueGuaranteeRequest request) {
        // 1. 校验客户资质
        Customer customer = customerRepository.findById(request.getApplicantId())
            .orElseThrow();

        // 2. 确定保函金额和期限
        BigDecimal guaranteeFee = calculateGuaranteeFee(
            request.getAmount(),
            request.getPeriod(),
            request.getGuaranteeType()
        );

        // 3. 冻结保证金或占用授信额度
        if (request.isUseMargin()) {
            // 使用保证金
            marginService.freezeMargin(
                customer.getId(),
                request.getAmount().multiply(new BigDecimal(request.getMarginRatio()))
            );
        } else {
            // 使用授信额度
            creditLimitService.freezeLimit(
                customer.getId(),
                LimitType.GUARANTEE,
                request.getAmount()
            );
        }

        // 4. 创建保函
        Guarantee guarantee = Guarantee.builder()
            .guaranteeNumber(request.getGuaranteeNumber())
            .applicantId(request.getApplicantId())
            .beneficiaryId(request.getBeneficiaryId())
            .amount(request.getAmount())
            .guaranteeType(request.getGuaranteeType())  // 投标/履约/预付款
            .validityStart(request.getStartDate())
            .validityEnd(request.getEndDate())
            .claimDeadline(request.getClaimDeadline())
            .status(GuaranteeStatus.ISSUED)
            .build();

        guaranteeRepository.save(guarantee);

        return GuaranteeIssueResult.success(guarantee);
    }

    /**
     * 保函索赔
     */
    @Transactional
    public GuaranteeClaimResult claimGuarantee(String guaranteeNumber, BigDecimal claimAmount) {
        Guarantee guarantee = guaranteeRepository.findByGuaranteeNumber(guaranteeNumber)
            .orElseThrow();

        // 1. 校验保函状态
        if (guarantee.getStatus() != GuaranteeStatus.ISSUED) {
            throw new GuaranteeStatusException("保函状态异常");
        }

        // 2. 校验索赔期限
        if (LocalDate.now().isAfter(guarantee.getClaimDeadline())) {
            throw new ClaimExpiredException("索赔期已过");
        }

        // 3. 校验索赔金额
        if (claimAmount.compareTo(guarantee.getAmount()) > 0) {
            throw new AmountExceedException("索赔金额超保函金额");
        }

        // 4. 付款
        paymentService.payToBeneficiary(
            guarantee.getBeneficiaryId(),
            claimAmount,
            "保函索赔-" + guaranteeNumber
        );

        // 5. 更新状态
        guarantee.setStatus(GuaranteeStatus.CLAIMED);
        guarantee.setClaimAmount(claimAmount);
        guarantee.setClaimDate(LocalDateTime.now());
        guaranteeRepository.save(guarantee);

        return GuaranteeClaimResult.success();
    }
}

2.4 企业贷款

2.4.1 额度管理

java
/**
 * 企业贷款额度服务
 */
@Service
public class CorporateLoanLimitService {

    /**
     * 计算企业可用贷款额度
     */
    public BigDecimal calculateAvailableLimit(String companyId) {
        // 获取授信额度
        BigDecimal creditLimit = getCreditLimit(companyId);

        // 减去已用额度
        BigDecimal usedLimit = loanRepository.sumUsedLimitByCompanyId(companyId);

        // 减去已审批未放款额度
        BigDecimal approvedNotDisbursed = loanRepository
            .sumApprovedNotDisbursedByCompanyId(companyId);

        return creditLimit.subtract(usedLimit).subtract(approvedNotDisbursed);
    }

    /**
     * 额度占用
     */
    @Transactional
    public void occupyLimit(String companyId, BigDecimal amount, String loanId) {
        LoanLimitOccupation occupation = LoanLimitOccupation.builder()
            .companyId(companyId)
            .loanId(loanId)
            .occupiedAmount(amount)
            .occupationType(OccupationType.APPROVED)
            .occupationDate(LocalDateTime.now())
            .build();

        occupationRepository.save(occupation);
    }

    /**
     * 额度释放
     */
    @Transactional
    public void releaseLimit(String companyId, BigDecimal amount, String loanId) {
        List<LoanLimitOccupation> occupations = occupationRepository
            .findByCompanyIdAndLoanId(companyId, loanId);

        for (LoanLimitOccupation occupation : occupations) {
            occupation.setStatus(OccupationStatus.RELEASED);
            occupation.setReleaseDate(LocalDateTime.now());
        }

        occupationRepository.saveAll(occupations);
    }
}

2.4.2 审批流程

java
/**
 * 企业贷款审批服务
 */
@Service
public class CorporateLoanApprovalService {

    /**
     * 提交贷款申请
     */
    @Transactional
    public LoanApplication submitApplication(LoanApplicationRequest request) {
        // 1. 校验企业客户
        CorporateCustomer customer = corporateCustomerRepository
            .findById(request.getCompanyId())
            .orElseThrow();

        // 2. 校验额度
        BigDecimal availableLimit = limitService.calculateAvailableLimit(
            request.getCompanyId());

        if (availableLimit.compareTo(request.getAmount()) < 0) {
            throw new InsufficientLimitException();
        }

        // 3. 占用额度
        limitService.occupyLimit(
            request.getCompanyId(),
            request.getAmount(),
            null  // 申请阶段未确定loanId
        );

        // 4. 创建申请
        LoanApplication application = LoanApplication.builder()
            .applicationNumber(generateApplicationNumber())
            .companyId(request.getCompanyId())
            .loanType(request.getLoanType())
            .amount(request.getAmount())
            .purpose(request.getPurpose())
            .term(request.getTerm())
            .status(ApplicationStatus.SUBMITTED)
            .currentStage(ApprovalStage.CREDIT_REVIEW)
            .build();

        applicationRepository.save(application);

        // 5. 触发审批流程
        approvalWorkflowService.startWorkflow(application);

        return application;
    }

    /**
     * 审批通过
     */
    @Transactional
    public void approve(String applicationId, String approverId, BigDecimal approvedAmount) {
        LoanApplication application = applicationRepository.findById(applicationId)
            .orElseThrow();

        // 更新申请状态
        application.setStatus(ApplicationStatus.APPROVED);
        application.setApprovedAmount(approvedAmount);
        application.setApproverId(approverId);
        application.setApprovalDate(LocalDateTime.now());

        applicationRepository.save(application);

        // 释放申请阶段占用的额度,重新占用放款额度
        limitService.releaseLimit(
            application.getCompanyId(),
            application.getAmount(),
            applicationId
        );

        limitService.occupyLimitForDisbursement(
            application.getCompanyId(),
            approvedAmount,
            application.getId()
        );
    }
}

三、数据流转

3.1 批量交易处理流程

企业银行的典型特点是大量批量操作,数据流如下:

┌────────────┐     ┌─────────────┐     ┌──────────────┐     ┌───────────┐
│  文件上传  │ ──→ │  文件解析  │ ──→ │  批量校验   │ ──→ │  执行处理 │
│ (CSV/Excel)│     │            │     │              │     │           │
└────────────┘     └─────────────┘     └──────────────┘     └───────────┘

                       ┌───────────────────────────────────────────┘

                ┌──────────────┐     ┌─────────────┐     ┌──────────┐
                │  日终统计    │ ──→ │  报表生成   │ ──> │  发送通知 │
                └──────────────┘     └─────────────┘     └──────────┘

3.2 贸易金融数据流

┌──────────┐     ┌────────────┐     ┌───────────┐     ┌──────────┐
│  开证申请 │ ──→ │  审批流程  │ ──→ │  开立LC   │ ──→ │ SWIFT发送 │
└──────────┘     └────────────┘     └───────────┘     └──────────┘

┌──────────┐     ┌────────────┐     ┌───────────┐     ┌──────────┐
│  交单议付 │ ──→ │  单据审核  │ ──→ │  付款/承兑 │ ──→ │  会计核算 │
└──────────┘     └────────────┘     └───────────┘     └──────────┘

四、技术架构与技术选型

4.1 批量处理架构

企业银行大量依赖批量处理,技术特点包括:

  • 分批处理:大文件拆分为小批次
  • 断点续传:失败后可从断点重试
  • 进度追踪:实时显示处理进度
java
/**
 * 批量处理器基类
 */
public abstract class BatchProcessor<T, R> {

    private static final int BATCH_SIZE = 1000;

    /**
     * 处理批量数据
     */
    public BatchProcessResult processBatch(List<T> items) {
        List<R> successResults = new ArrayList<>();
        List<BatchFailure<T>> failures = new ArrayList<>();

        // 分批处理
        List<List<T>> batches = Lists.partition(items, BATCH_SIZE);

        for (int i = 0; i < batches.size(); i++) {
            List<T> batch = batches.get(i);

            try {
                List<R> results = processSingleBatch(batch);
                successResults.addAll(results);
            } catch (Exception e) {
                // 记录失败项
                for (T item : batch) {
                    failures.add(BatchFailure.of(item, e.getMessage()));
                }
            }

            // 更新进度
            updateProgress(i + 1, batches.size());
        }

        return BatchProcessResult.builder()
            .totalCount(items.size())
            .successCount(successResults.size())
            .failureCount(failures.size())
            .failures(failures)
            .build();
    }

    /**
     * 处理单批次(子类实现)
     */
    protected abstract List<R> processSingleBatch(List<T> batch);
}

4.2 与SWIFT系统对接

国际贸易金融大量使用SWIFT报文:

java
/**
 * SWIFT报文服务
 */
@Service
public class SwiftMessageService {

    /**
     * 发送SWIFT报文
     */
    public void sendSwiftMessage(SwiftMessage message) {
        // 1. 报文格式校验
        swiftValidator.validate(message);

        // 2. 报文加密
        String encryptedContent = encryptionService.encrypt(message.getContent());

        // 3. 发送报文
        swiftConnector.send(encryptedContent);

        // 4. 记录发送日志
        swiftLogRepository.save(SwiftMessageLog.builder()
            .messageType(message.getType())
            .reference(message.getReference())
            .sendTime(LocalDateTime.now())
            .status(SwiftStatus.SENT)
            .build());
    }

    /**
     * 接收SWIFT报文
     */
    @Transactional
    public void receiveSwiftMessage(String rawMessage) {
        // 1. 报文解密
        String content = encryptionService.decrypt(rawMessage);

        // 2. 解析报文
        SwiftMessage message = swiftParser.parse(content);

        // 3. 处理报文
        processSwiftMessage(message);

        // 4. 更新日志
        swiftLogRepository.updateStatus(
            message.getReference(),
            SwiftStatus.RECEIVED
        );
    }

    /**
     * 处理MT103(客户汇款)
     */
    private void processMT103(MT103 message) {
        // 根据报文内容执行相应业务
        if ("708".equals(message.getTransactionType())) {
            // 开立信用证
            letterOfCreditService.processMT103(message);
        } else {
            // 普通汇款
            inboundRemittanceService.processMT103(message);
        }
    }
}

五、典型开发场景

5.1 企业贷款申请流程

java
@RestController
@RequestMapping("/api/v1/corporate-loan")
public class CorporateLoanController {

    @Autowired
    private CorporateLoanService loanService;

    /**
     * 提交贷款申请
     */
    @PostMapping("/apply")
    public ResponseEntity<ApplicationResult> apply(
            @RequestHeader("X-Company-Id") String companyId,
            @RequestBody LoanApplicationRequest request) {

        // 校验
        if (request.getAmount().compareTo(new BigDecimal("10000000")) > 0) {
            // 大额贷款需要额外材料
            validateLargeLoanApplication(request);
        }

        // 提交申请
        LoanApplication application = loanService.submitApplication(
            companyId, request);

        return ResponseEntity.ok(ApplicationResult.success(application));
    }

    /**
     * 查询申请进度
     */
    @GetMapping("/application/{applicationNumber}/status")
    public ResponseEntity<ApplicationStatusVO> getApplicationStatus(
            @PathVariable String applicationNumber) {

        ApplicationStatusVO status = loanService.getApplicationStatus(applicationNumber);
        return ResponseEntity.ok(status);
    }
}

5.2 定时任务开发

java
@Component
public class CMBatchTaskScheduler {

    @Autowired
    private CashManagementService cashManagementService;

    @Autowired
    private TradeFinanceService tradeFinanceService;

    /**
     * 每日定时任务
     */
    @Scheduled(cron = "0 30 1 * * ?")  // 凌晨1:30
    public void dailyTask() {
        log.info("开始执行CM日终任务");

        // 1. 资金归集
        cashManagementService.executeCashConcentration();

        // 2. 信用证到期检查
        tradeFinanceService.checkLCExpiry();

        // 3. 保函到期检查
        tradeFinanceService.checkGuaranteeExpiry();

        // 4. 贷款利息计提
        loanService.accrueInterest();

        // 5. 生成对账文件
        reconciliationService.generateReconciliationFile();

        log.info("CM日终任务完成");
    }

    /**
     * 每小时执行的任务
     */
    @Scheduled(cron = "0 0 * * * ?")
    public void hourlyTask() {
        // 处理代收代付结果
        cashManagementService.processCollectionResult();
        cashManagementService.processPaymentResult();
    }
}

六、业务难点与解决方案

6.1 批量交易的事务处理

问题:批量交易中部分成功部分失败如何处理?

解决方案:采用事后补偿机制

java
/**
 * 批量交易事后补偿
 */
@Service
public class BatchCompensationService {

    /**
     * 补偿失败的批量交易
     */
    @Transactional
    public void compensateFailures(String batchId) {
        BatchTransfer batch = batchTransferRepository.findById(batchId)
            .orElseThrow();

        List<TransferDetail> failures = batch.getDetails().stream()
            .filter(d -> d.getStatus() == TransferStatus.FAILED)
            .collect(Collectors.toList());

        for (TransferDetail detail : failures) {
            try {
                // 重试
                singleTransferService.transfer(
                    batch.getFromAccountId(),
                    detail.getToAccountNumber(),
                    detail.getAmount(),
                    detail.getRemark()
                );
                detail.setStatus(TransferStatus.COMPENSATED);
            } catch (Exception e) {
                // 继续下一个
                log.error("补偿失败 detailId={}", detail.getId(), e);
            }
        }

        batchTransferRepository.save(batch);
    }
}

6.2 贸易金融的合规检查

问题:贸易金融涉及大量合规检查,如何高效处理?

解决方案:规则引擎 + 缓存

java
/**
 * 贸易金融合规检查服务
 */
@Service
public class TradeComplianceService {

    @Autowired
    private RuleEngine ruleEngine;

    /**
     * 开证合规检查
     */
    public ComplianceCheckResult checkLCCompliance(OpenLCRequest request) {
        // 1. 获取适用的合规规则
        List<ComplianceRule> rules = complianceRuleRepository
            .findByProductTypeAndStatus(
                ProductType.LC,
                RuleStatus.ACTIVE
            );

        // 2. 逐条检查
        List<ComplianceViolation> violations = new ArrayList<>();
        for (ComplianceRule rule : rules) {
            boolean passed = ruleEngine.evaluate(rule, request);
            if (!passed) {
                violations.add(ComplianceViolation.builder()
                    .ruleId(rule.getId())
                    .ruleName(rule.getName())
                    .description(rule.getDescription())
                    .build());
            }
        }

        return ComplianceCheckResult.builder()
            .passed(violations.isEmpty())
            .violations(violations)
            .build();
    }
}

七、合规要求

7.1 反洗钱要求

企业客户的反洗钱要求比个人客户更严格:

java
/**
 * 企业客户反洗钱服务
 */
@Service
public class CorporateAmlService {

    /**
     * 企业客户强化尽职调查(EDD)
     */
    public void performEnhancedDueDiligence(String companyId) {
        CorporateCustomer customer = corporateCustomerRepository
            .findById(companyId)
            .orElseThrow();

        // 1. 受益所有人识别
        List<BeneficialOwner> owners = identifyBeneficialOwners(customer);

        // 2. 受益所有人筛查
        screeningService.screenBeneficialOwners(owners);

        // 3. 高风险国家筛查
        if (customer.hasHighRiskCountryOperation()) {
            screeningService.screenHighRiskCountry(customer);
        }

        // 4. 创建强化尽职调查记录
        EnhancedDueDiligence edd = EnhancedDueDiligence.builder()
            .companyId(companyId)
            .investigationDate(LocalDateTime.now())
            .beneficialOwners(owners)
            .riskRating(customer.getRiskRating())
            .status(EDDStatus.COMPLETED)
            .build();

        eddRepository.save(edd);
    }

    /**
     * 大额可疑交易报告
     */
    public void reportSuspiciousTransaction(Transaction transaction) {
        // 触发可疑交易规则
        List<String> suspiciousReasons = checkSuspiciousRules(transaction);

        if (!suspiciousReasons.isEmpty()) {
            SuspiciousTransactionReport report = SuspiciousTransactionReport.builder()
                .transactionId(transaction.getId())
                .reportDate(LocalDateTime.now())
                .reasons(suspiciousReasons)
                .status(ReportStatus.PENDING)
                .build();

            reportRepository.save(report);

            // 上报反洗钱系统
            amlSystem.reportSuspiciousTransaction(report);
        }
    }
}

7.2 制裁筛查

java
/**
 * 制裁筛查服务
 */
@Service
public class SanctionsScreeningService {

    /**
     * 筛查交易对手
     */
    public ScreeningResult screenCounterparty(String name, String country) {
        // 1. 精确匹配
        List<SanctionsMatch> exactMatches = sanctionsRepository.findExactMatch(name, country);

        // 2. 模糊匹配
        List<SanctionsMatch> fuzzyMatches = sanctionsRepository.findFuzzyMatch(name);

        // 3. 构建结果
        return ScreeningResult.builder()
            .screenedName(name)
            .screenedCountry(country)
            .isHit(!exactMatches.isEmpty() || !fuzzyMatches.isEmpty())
            .exactMatches(exactMatches)
            .fuzzyMatches(fuzzyMatches)
            .screeningTime(LocalDateTime.now())
            .build();
    }
}

八、常见问题与避坑指南

8.1 批量操作内存溢出

:一次性加载大文件导致内存溢出

java
// ❌ 错误示例
public void processBatch(MultipartFile file) {
    List<String> lines = Files.readAllLines(file.toPath());  // 大文件会OOM
    // 处理...
}

// ✅ 正确示例
public void processBatch(MultipartFile file) {
    try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(file.getInputStream()))) {
        String line;
        while ((line = reader.readLine()) != null) {
            // 逐行处理
            processLine(line);
        }
    }
}

8.2 授权规则配置错误

:授权规则配置导致交易无法执行

java
// ✅ 注意要点
// 1. 授权规则要支持配置化,不能硬编码
// 2. 配置生效要有审核流程
// 3. 测试环境要模拟各种授权场景

8.3 贸易金融状态机混乱

:信用证状态转换逻辑错误

java
// ✅ 正确做法:使用状态机框架
public enum LCStatus {
    DRAFT,           // 草稿
    SUBMITTED,       // 已提交
    APPROVED,        // 已审批
    REJECTED,        // 已拒绝
    OPENED,          // 已开立
    AMENDED,         // 已修改
    NEGOTIATED,      // 已议付
    PAID,            // 已付款
    CLOSED           // 已关闭
}

// 状态转换规则
public class LCStatusTransition {
    public static final Map<LCStatus, Set<LCStatus>> ALLOWED_TRANSITIONS =
        Map.of(
            LCStatus.DRAFT, Set.of(LCStatus.SUBMITTED),
            LCStatus.SUBMITTED, Set.of(LCStatus.APPROVED, LCStatus.REJECTED),
            LCStatus.APPROVED, Set.of(LCStatus.OPENED),
            // ...
        );

    public static boolean canTransition(LCStatus from, LCStatus to) {
        return ALLOWED_TRANSITIONS.getOrDefault(from, Set.of()).contains(to);
    }
}

九、总结

工商金融业务(CMB)的技术特点:

  1. 批量处理为主:大量代收代付、批量转账操作
  2. 多授权人体系:企业账户需要复杂的多级授权机制
  3. 贸易金融复杂:信用证、保函等业务逻辑复杂
  4. 合规要求严格:企业客户的反洗钱要求更高

开发建议:

  • 重视批量处理性能和错误处理
  • 仔细理解业务状态机
  • 注意与企业核心系统的交互
  • 上线前充分测试各种业务场景

本文档持续更新中,如有疑问欢迎交流讨论。

> 学而时习之,不亦说乎?