工商金融业务(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)的技术特点:
- 批量处理为主:大量代收代付、批量转账操作
- 多授权人体系:企业账户需要复杂的多级授权机制
- 贸易金融复杂:信用证、保函等业务逻辑复杂
- 合规要求严格:企业客户的反洗钱要求更高
开发建议:
- 重视批量处理性能和错误处理
- 仔细理解业务状态机
- 注意与企业核心系统的交互
- 上线前充分测试各种业务场景
本文档持续更新中,如有疑问欢迎交流讨论。