云计算百科
云计算领域专业知识百科平台

企业级Java项目金融应用领域——银行系统

银行系统

后端

  • 核心框架: Spring Boot/Spring Cloud (微服务架构)

  • 持久层: MyBatis/JPA, Hibernate

  • 数据库: Oracle/MySQL (主从复制), Redis (缓存)

  • 消息队列: RabbitMQ/Kafka (异步处理)

  • API接口: RESTful API, Swagger文档

  • 安全框架: Spring Security, OAuth2/JWT

  • 分布式事务: Seata

  • 搜索引擎: Elasticsearch (交易查询)

  • 批处理: Spring Batch

前端

  • Web框架: Vue.js/React + Element UI/Ant Design

  • 移动端: 原生APP或React Native/Flutter

  • 图表库: ECharts/D3.js (数据可视化)

**其他:**分布式锁: Redisson 分布式ID生成: Snowflake算法 文件处理: Apache POI (Excel), PDFBox 工作流引擎: Activiti/Camunda

  • 容器化: Docker + Kubernetes

  • 服务发现: Nacos/Eureka

  • 配置中心: Apollo/Nacos

  • 网关: Spring Cloud Gateway

  • 监控: Prometheus + Grafana

  • 日志: ELK Stack (Elasticsearch, Logstash, Kibana)

  • CI/CD: Jenkins/

  • GitLab CI

1. 账户管理

账户开户/销户

账户信息维护

账户状态管理(冻结/解冻)

账户余额查询

账户分级管理(个人/企业)

<dependencies>
<!– Spring Boot Starter –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!– Database –>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

<!– Lombok –>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!– Spring Security –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!– JWT支持 –>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>

<!– 数据加密 –>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>

<!– 防XSS –>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
</dependency>

<!– 限流 –>
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>7.6.0</version>
</dependency>
<!– Other –>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.14</version>
</dependency>
</dependencies>

spring:
datasource:
url: jdbc:mysql://localhost:3306/bank_account_db?useSSL=false&serverTimezone=UTC
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect

server:
port: 8080

logging:
level:
com.bank.account: DEBUG
security:
jwt:
secret-key: your256bitsecretkeychangethistosomethingsecure
expiration: 86400000 # 24 hours in milliseconds
refresh-token.expiration: 604800000 # 7 days in milliseconds
encryption:
key: yourencryptionkey32bytes
iv: yourinitializationvector16bytes
rate-limit:
enabled: true
capacity: 100
refill-rate: 100
refill-time: 1 # minutes

@Entity
@Table(name="bank_account")
@Data
public class Account{

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;

@Column(unique=true,nullable=false)
private String accountNumber;

@Column(nullable = false)
private Long customerId;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AccountType accountType;//枚举类型:个人储蓄/活期,企业活期/贷款,信用卡

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AccountStatus status;//枚举状态:活跃、不活跃、冻结、已关闭、休眠

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal balance = BigDecimal.ZERO;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal availableBalance = BigDecimal.ZERO;

@Column(nullable = false)
private String currency = "CNY";

@CreationTimestamp
private Date createdAt;

@UpdateTimestamp
private Date updatedAt;

@Version
private Long version; // 乐观锁版本号
}

/**
安全相关的实体
*/

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String username;

@Column(nullable = false)
private String password;

@Enumerated(EnumType.STRING)
private Role role;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role.name()));
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}

public enum Role {
CUSTOMER,
TELLER,
MANAGER,
ADMIN
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationRequest {
private String username;
private String password;
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationResponse {
private String token;
private String refreshToken;
}

public enum AccountType {
PERSONAL_SAVINGS, // 个人储蓄账户
PERSONAL_CURRENT, // 个人活期账户
CORPORATE_CURRENT, // 企业活期账户
CORPORATE_LOAN, // 企业贷款账户
CREDIT_CARD // 信用卡账户
}

public enum AccountStatus {
ACTIVE, // 活跃
INACTIVE, // 不活跃
FROZEN, // 冻结
CLOSED, // 已关闭
DORMANT // 休眠
}

@Data
public class AccountDTO {
private Long id;
private String accountNumber;
private Long customerId;
private AccountType accountType;
private AccountStatus status;
private BigDecimal balance;
private BigDecimal availableBalance;
private String currency;
private Date createdAt;
private Date updatedAt;
}

@Data
public class CreateAccountRequest {
@NotNull
private Long customerId;

@NotNull
private AccountType accountType;

private String currency = "CNY";
}

@Data
public class AccountOperationResponse {
private boolean success;
private String message;
private String accountNumber;
private BigDecimal newBalance;
}

@Getter
public class AccountException extends RuntimeException {
private final ErrorCode errorCode;

public AccountException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}

@Getter
public enum ErrorCode {
ACCOUNT_NOT_FOUND(404, "Account not found"),
ACCOUNT_NOT_ACTIVE(400, "Account is not active"),
ACCOUNT_ALREADY_FROZEN(400, "Account is already frozen"),
ACCOUNT_NOT_FROZEN(400, "Account is not frozen"),
ACCOUNT_ALREADY_CLOSED(400, "Account is already closed"),
ACCOUNT_BALANCE_NOT_ZERO(400, "Account balance is not zero"),
INSUFFICIENT_BALANCE(400, "Insufficient balance"),
INVALID_AMOUNT(400, "Amount must be positive");

private final int status;
private final String message;

ErrorCode(int status, String message) {
this.status = status;
this.message = message;
}
}

@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AccountException.class)
public ResponseEntity<ErrorResponse> handleAccountException(AccountException e) {
ErrorCode errorCode = e.getErrorCode();
return ResponseEntity
.status(errorCode.getStatus())
.body(new ErrorResponse(errorCode.getStatus(), errorCode.getMessage()));
}
}

安全配置类和限流配置类

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/auth/**",
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html"
).permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("https://bank.com", "https://admin.bank.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setExposedHeaders(List.of("X-Rate-Limit-Remaining"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

@Configuration
@EnableCaching
public class RateLimitConfig {
@Bean
public CacheManager cacheManager() {
CacheManager cacheManager = Caching.getCachingProvider().getCacheManager();
MutableConfiguration<String, byte[]> config = new MutableConfiguration<>();
cacheManager.createCache("rate-limit-buckets", config);
return cacheManager;
}

@Bean
ProxyManager<String> proxyManager(CacheManager cacheManager) {
return new JCacheProxyManager<>(cacheManager.getCache("rate-limit-buckets"));
}

@Bean
public BucketConfiguration bucketConfiguration() {
return BucketConfiguration.builder()
.addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))))
.build();
}

@Bean
public Bucket bucket(ProxyManager<String> proxyManager, BucketConfiguration bucketConfiguration) {
return proxyManager.builder().build("global-limit", bucketConfiguration);
}
}

JWT认证实现

@Service
public class JwtService {
@Value("${security.jwt.secret-key}")
private String secretKey;

@Value("${security.jwt.expiration}")
private long jwtExpiration;

@Value("${security.jwt.refresh-token.expiration}")
private long refreshExpiration;

public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}

public String generateToken(
Map<String, Object> extraClaims,
UserDetails userDetails
) {
return buildToken(extraClaims, userDetails, jwtExpiration);
}

public String generateRefreshToken(
UserDetails userDetails
) {
return buildToken(new HashMap<>(), userDetails, refreshExpiration);
}

private String buildToken(
Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration
) {
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}

public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}

private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}

private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}

工具类

/**
账号生成工具
*/

@Component
public class AccountNumberGenerator {
private static final String BANK_CODE = "888";
private final AtomicLong sequence = new AtomicLong(1);

public String generate(AccountType accountType) {
long seq = sequence.getAndIncrement();
String prefix = getAccountPrefix(accountType);
String seqStr = String.format("%010d", seq);

// 简单校验码计算
String rawNumber = BANK_CODE + prefix + seqStr;
int checkDigit = calculateCheckDigit(rawNumber);

return rawNumber + checkDigit;
}

private String getAccountPrefix(AccountType accountType) {
return switch (accountType) {
case PERSONAL_SAVINGS -> "10";
case PERSONAL_CURRENT -> "11";
case CORPORATE_CURRENT -> "20";
case CORPORATE_LOAN -> "21";
case CREDIT_CARD -> "30";
};
}

private int calculateCheckDigit(String number) {
int sum = 0;
for (int i = 0; i < number.length(); i++) {
int digit = Character.getNumericValue(number.charAt(i));
sum += (i % 2 == 0) ? digit * 1 : digit * 3;
}
return (10 (sum % 10)) % 10;
}
}

/**
数据加密工具
*/

@Component
public class EncryptionUtil {
@Value("${security.encryption.key}")
private String encryptionKey;

@Value("${security.encryption.iv}")
private String iv;

static {
Security.addProvider(new BouncyCastleProvider());
}

public String encrypt(String data) {
try {
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
SecretKeySpec keySpec = new SecretKeySpec(encryptionKey.getBytes(StandardCharsets.UTF_8), "AES");

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);

byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("Encryption failed", e);
}
}

public String decrypt(String encryptedData) {
try {
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
SecretKeySpec keySpec = new SecretKeySpec(encryptionKey.getBytes(StandardCharsets.UTF_8), "AES");

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);

byte[] decoded = Base64.getDecoder().decode(encryptedData);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Decryption failed", e);
}
}
}

防XSS过滤器

@Component
public class XSSFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
filterChain.doFilter(new XSSRequestWrapper(request), response);
}

private static class XSSRequestWrapper extends HttpServletRequestWrapper {
private final Map<String, String[]> escapedParameterValuesMap = new ConcurrentHashMap<>();

public XSSRequestWrapper(HttpServletRequest request) {
super(request);
}

@Override
public String getParameter(String name) {
String parameter = super.getParameter(name);
return parameter != null ? StringEscapeUtils.escapeHtml4(parameter) : null;
}

@Override
public String[] getParameterValues(String name) {
String[] parameterValues = super.getParameterValues(name);
if (parameterValues == null) {
return null;
}

return escapedParameterValuesMap.computeIfAbsent(name, k -> {
String[] escapedValues = new String[parameterValues.length];
for (int i = 0; i < parameterValues.length; i++) {
escapedValues[i] = StringEscapeUtils.escapeHtml4(parameterValues[i]);
}
return escapedValues;
});
}

@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> parameterMap = super.getParameterMap();
Map<String, String[]> escapedParameterMap = new ConcurrentHashMap<>();

for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
escapedParameterMap.put(entry.getKey(), getParameterValues(entry.getKey()));
}

return escapedParameterMap;
}

@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(getParameterMap().keySet());
}
}
}

Controller层

@RestController
@RequestMapping("/api/accounts")
@RequiredArgsConstructor
public class AccountController {
private final AccountService accountService;

@PostMapping
public ResponseEntity<AccountDTO> createAccount(@Valid @RequestBody CreateAccountRequest request){
AccountDTO account = accountService.createAccount(request);
return ResponseEntity.ok(account);
}

@GetMapping("/{accountNumber}")
public ResponseEntity<AccountDTO> getAccount(@PathVariable String accountNumber) {
AccountDTO account = accountService.getAccount(accountNumber);
return ResponseEntity.ok(account);
}

@PostMapping("/{accountNumber}/deposit")
public ResponseEntity<AccountOperationResponse> deposit(
@PathVariable String accountNumber,
@RequestParam BigDecimal amount) {
AccountOperationResponse response = accountService.deposit(accountNumber, amount);
return ResponseEntity.ok(response);
}

@PostMapping("/{accountNumber}/withdraw")
public ResponseEntity<AccountOperationResponse> withdraw(
@PathVariable String accountNumber,
@RequestParam BigDecimal amount) {
AccountOperationResponse response = accountService.withdraw(accountNumber, amount);
return ResponseEntity.ok(response);
}

@PostMapping("/{accountNumber}/freeze")
public ResponseEntity<AccountOperationResponse> freezeAccount(@PathVariable String accountNumber) {
AccountOperationResponse response = accountService.freezeAccount(accountNumber);
return ResponseEntity.ok(response);
}

@PostMapping("/{accountNumber}/unfreeze")
public ResponseEntity<AccountOperationResponse> unfreezeAccount(@PathVariable String accountNumber) {
AccountOperationResponse response = accountService.unfreezeAccount(accountNumber);
return ResponseEntity.ok(response);
}

@PostMapping("/{accountNumber}/close")
public ResponseEntity<AccountOperationResponse> closeAccount(@PathVariable String accountNumber) {
AccountOperationResponse response = accountService.closeAccount(accountNumber);
return ResponseEntity.ok(response);
}
}

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService authenticationService;

@PostMapping("/register/customer")
public ResponseEntity<AuthenticationResponse> registerCustomer(
@RequestBody AuthenticationRequest request
) {
return ResponseEntity.ok(authenticationService.register(request, Role.CUSTOMER));
}

@PostMapping("/register/teller")
public ResponseEntity<AuthenticationResponse> registerTeller(
@RequestBody AuthenticationRequest request
) {
return ResponseEntity.ok(authenticationService.register(request, Role.TELLER));
}

@PostMapping("/authenticate")
public ResponseEntity<AuthenticationResponse> authenticate(
@RequestBody AuthenticationRequest request
) {
return ResponseEntity.ok(authenticationService.authenticate(request));
}
}

Service层

@Service
@RequiredArgsConstructor
@Slf4j
public class AccountService{

private final AccountRepository accountRepository;
private final AccountNumberGenerator accountNumberGenerator;

private final EncryptionUtil encryptionUtil;
private final RateLimitConfig rateLimitConfig;

@Transactional
public AccountDTO createAccount(CreateAccountRequest request){

String accountNumber = accountNumberGenerator.generate(request.getAccountType());
Account account = new Account();
account.setAccountNumber(accountNumber);
account.setCustomerId(request.getCustomerId());
account.setAccountType(request.getAccountType());
account.setStatus(AccountStatus.ACTIVE);
account.setCurrency(request.getCurrency());

Account savedAccount = accountRepository.save(account);
log.info("Account created: {}", accountNumber);

return convertToDTO(savedAccount);
}

@Transactional(readOnly = true)
@PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN') || "
+ "(hasRole('CUSTOMER') && @accountSecurityService.isAccountOwner(authentication, #accountNumber))")
public AccountDTO getAccount(String accountNumber) {

// 限流检查
Bucket bucket = rateLimitConfig.bucket();
if (!bucket.tryConsume(1)) {
throw new AccountException(ErrorCode.TOO_MANY_REQUESTS);
}
Account account = accountRepository.findByAccountNumber(accountNumber)
.orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));

//敏感数据加密
AccountDTO dto = convertToDto(account);
dto.setAccountNumber(encryptionUtil.encrypt(dto.getAccountNumber()));
return dto;
}

@Transactional
@PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN')")
public AccountOperationResponse deposit(String accountNumber, BigDecimal amount) {

//解密账号
String decryptedAccountNumber = encryptionUtil.decrypt(accountNumber);
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new AccountException(ErrorCode.INVALID_AMOUNT);
}

Account account = accountRepository.findByAccountNumberForUpdate(accountNumber)
.orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));

if (account.getStatus() != AccountStatus.ACTIVE) {
throw new AccountException(ErrorCode.ACCOUNT_NOT_ACTIVE);
}

BigDecimal newBalance = account.getBalance().add(amount);
account.setBalance(newBalance);
account.setAvailableBalance(newBalance);

accountRepository.save(account);
log.info("Deposit {} to account {}", amount, accountNumber);

// 审计日志
logSecurityEvent("DEPOSIT", decryptedAccountNumber, amount);

return buildSuccessResponse(accountNumber, newBalance, "Deposit successful");
}

@Transactional
public AccountOperationResponse withdraw(String accountNumber, BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new AccountException(ErrorCode.INVALID_AMOUNT);
}

Account account = accountRepository.findByAccountNumberForUpdate(accountNumber)
.orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));

if (account.getStatus() != AccountStatus.ACTIVE) {
throw new AccountException(ErrorCode.ACCOUNT_NOT_ACTIVE);
}

if (account.getAvailableBalance().compareTo(amount) < 0) {
throw new AccountException(ErrorCode.INSUFFICIENT_BALANCE);
}

BigDecimal newBalance = account.getBalance().subtract(amount);
account.setBalance(newBalance);
account.setAvailableBalance(newBalance);

accountRepository.save(account);
log.info("Withdraw {} from account {}", amount, accountNumber);

return buildSuccessResponse(accountNumber, newBalance, "Withdrawal successful");
}

@Transactional
public AccountOperationResponse freezeAccount(String accountNumber) {
Account account = accountRepository.findByAccountNumberForUpdate(accountNumber)
.orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));

if (account.getStatus() == AccountStatus.FROZEN) {
throw new AccountException(ErrorCode.ACCOUNT_ALREADY_FROZEN);
}

account.setStatus(AccountStatus.FROZEN);
accountRepository.save(account);
log.info("Account {} frozen", accountNumber);

return buildSuccessResponse(accountNumber, account.getBalance(), "Account frozen successfully");
}

@Transactional
public AccountOperationResponse unfreezeAccount(String accountNumber) {
Account account = accountRepository.findByAccountNumberForUpdate(accountNumber)
.orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));

if (account.getStatus() != AccountStatus.FROZEN) {
throw new AccountException(ErrorCode.ACCOUNT_NOT_FROZEN);
}

account.setStatus(AccountStatus.ACTIVE);
accountRepository.save(account);
log.info("Account {} unfrozen", accountNumber);

return buildSuccessResponse(accountNumber, account.getBalance(), "Account unfrozen successfully");
}

@Transactional
public AccountOperationResponse closeAccount(String accountNumber) {
Account account = accountRepository.findByAccountNumberForUpdate(accountNumber)
.orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));

if (account.getStatus() == AccountStatus.CLOSED) {
throw new AccountException(ErrorCode.ACCOUNT_ALREADY_CLOSED);
}

if (account.getBalance().compareTo(BigDecimal.ZERO) != 0) {
throw new AccountException(ErrorCode.ACCOUNT_BALANCE_NOT_ZERO);
}

account.setStatus(AccountStatus.CLOSED);
accountRepository.save(account);
log.info("Account {} closed", accountNumber);

return buildSuccessResponse(accountNumber, account.getBalance(), "Account closed successfully");
}

private AccountDTO convertToDTO(Account account) {
AccountDTO dto = new AccountDTO();
dto.setId(account.getId());
dto.setAccountNumber(account.getAccountNumber());
dto.setCustomerId(account.getCustomerId());
dto.setAccountType(account.getAccountType());
dto.setStatus(account.getStatus());
dto.setBalance(account.getBalance());
dto.setAvailableBalance(account.getAvailableBalance());
dto.setCurrency(account.getCurrency());
dto.setCreatedAt(account.getCreatedAt());
dto.setUpdatedAt(account.getUpdatedAt());
return dto;
}

private AccountOperationResponse buildSuccessResponse(String accountNumber, BigDecimal newBalance, String message) {
AccountOperationResponse response = new AccountOperationResponse();
response.setSuccess(true);
response.setMessage(message);
response.setAccountNumber(accountNumber);
response.setNewBalance(newBalance);
return response;
}
}

/**
安全服务
*/

@Service
@RequiredArgsConstructor
public class AuthenticationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final AuthenticationManager authenticationManager;

public AuthenticationResponse register(AuthenticationRequest request, Role role) {
var user = User.builder()
.username(request.getUsername())
.password(passwordEncoder.encode(request.getPassword()))
.role(role)
.build();
userRepository.save(user);

var jwtToken = jwtService.generateToken(user);
var refreshToken = jwtService.generateRefreshToken(user);

return AuthenticationResponse.builder()
.token(jwtToken)
.refreshToken(refreshToken)
.build();
}

public AuthenticationResponse authenticate(AuthenticationRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);

var user = userRepository.findByUsername(request.getUsername())
.orElseThrow();

var jwtToken = jwtService.generateToken(user);
var refreshToken = jwtService.generateRefreshToken(user);

return AuthenticationResponse.builder()
.token(jwtToken)
.refreshToken(refreshToken)
.build();
}
}

Repository层

public interface AccountRepository extends JpaRepository<Account,Long>{

Optional<Account> findByAccountNumber(String accountNumber);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.accountNumber = :accountNumber")
Optional<Account> findByAccountNumberForUpdate(@Param("accountNumber") String accountNumber);

boolean existsByAccountNumber(String accountNumber);
}

2. 交易处理

存款/取款

转账(同行/跨行)

批量交易处理

交易流水记录

交易限额管理

# 在原有配置基础上添加
service:
account:
url: http://accountservice:8080

security:
transaction:
max-retry-attempts: 3
retry-delay: 1000 # ms

@Entity
@Table(name = "transaction")
@Data
public class Transaction {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String transactionId;

@Column(nullable = false)
private String accountNumber;

@Column
private String counterpartyAccountNumber;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private TransactionType transactionType;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private TransactionStatus status;//处理中、已完成、失败、已冲正、已取消

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal amount;

@Column(precision = 19, scale = 4)
private BigDecimal fee;

@Column(nullable = false)
private String currency = "CNY";

@Column
private String description;

@Column(nullable = false)
private String reference;

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;

@Version
private Long version;
}

@Entity
@Table(name = "transaction_limit")
@Data
public class TransactionLimit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private TransactionType transactionType;//存款、取款、转账、账单支付、贷款还款、费用收取

@Column(nullable = false)
private String accountType;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal dailyLimit;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal perTransactionLimit;

@Column(nullable = false)
private Integer dailyCountLimit;
}

@Data
public class TransactionDTO {
private String transactionId;
private String accountNumber;
private String counterpartyAccountNumber;
private TransactionType transactionType;
private TransactionStatus status;
private BigDecimal amount;
private BigDecimal fee;
private String currency;
private String description;
private String reference;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

@Data
public class DepositRequest {
@NotNull
private String accountNumber;

@NotNull
@Positive
private BigDecimal amount;

private String description;
}

@Data
public class WithdrawalRequest {
@NotNull
private String accountNumber;

@NotNull
@Positive
private BigDecimal amount;

private String description;
}

@Data
public class TransferRequest {
@NotNull
private String fromAccountNumber;

@NotNull
private String toAccountNumber;

@NotNull
@Positive
private BigDecimal amount;

private String description;
}

@Data
public class BatchTransactionRequest {
@NotEmpty
@Valid
private List<TransferRequest> transactions;
}

异常

@Getter
public enum ErrorCode {
// 原有错误码…
TRANSACTION_LIMIT_NOT_FOUND(400, "Transaction limit not found"),
EXCEED_PER_TRANSACTION_LIMIT(400, "Exceed per transaction limit"),
EXCEED_DAILY_LIMIT(400, "Exceed daily limit"),
EXCEED_DAILY_COUNT_LIMIT(400, "Exceed daily count limit"),
DEPOSIT_FAILED(400, "Deposit failed"),
WITHDRAWAL_FAILED(400, "Withdrawal failed"),
TRANSFER_FAILED(400, "Transfer failed"),
ACCOUNT_SERVICE_UNAVAILABLE(503, "Account service unavailable"),
TRANSACTION_SERVICE_UNAVAILABLE(503, "Transaction service unavailable");

private final int status;
private final String message;

ErrorCode(int status, String message) {
this.status = status;
this.message = message;
}
}

工具类

@Component
public class TransactionIdGenerator {
private static final String BANK_CODE = "888";
private final AtomicLong sequence = new AtomicLong(1);

public String generate() {
long timestamp = Instant.now().toEpochMilli();
long seq = sequence.getAndIncrement();
return String.format("%s-TRX-%d-%06d", BANK_CODE, timestamp, seq);
}
}

Controller层

@Controller
@RequestMapping("/api/transactions")
@RequiredArgsConstructor
public class TransactionController{

private final TransactionService transactionService;

@PostMapping("/deposit")
@PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN')")
public ResponseEntity<TransactionDTO> deposit(@Valid @RequestBody DepositRequest request) {
return ResponseEntity.ok(transactionService.deposit(request));
}

@PostMapping("/withdraw")
public ResponseEntity<TransactionDTO> withdraw(@Valid @RequestBody WithdrawalRequest request) {
return ResponseEntity.ok(transactionService.withdraw(request));
}

@PostMapping("/transfer")
public ResponseEntity<TransactionDTO> transfer(@Valid @RequestBody TransferRequest request) {
return ResponseEntity.ok(transactionService.transfer(request));
}

@PostMapping("/batch-transfer")
@PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN')")
public ResponseEntity<List<TransactionDTO>> batchTransfer(@Valid @RequestBody BatchTransactionRequest request) {
return ResponseEntity.ok(transactionService.batchTransfer(request));
}

@GetMapping("/history")
public ResponseEntity<List<TransactionDTO>> getTransactionHistory(
@RequestParam String accountNumber,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
return ResponseEntity.ok(transactionService.getTransactionHistory(accountNumber, startDate, endDate));
}

}

Service层

@Service
@RequiredArgsConstructor
@Slf4j
public class TransactionService {
private final TransactionRepository transactionRepository;
private final TransactionLimitRepository limitRepository;
private final AccountClientService accountClientService;
private final TransactionIdGenerator idGenerator;
private final EncryptionUtil encryptionUtil;

@Transactional
@PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN')")
public TransactionDTO deposit(DepositRequest request) {
// 解密账号
String decryptedAccountNumber = encryptionUtil.decrypt(request.getAccountNumber());

// 验证金额
validateAmount(request.getAmount());

// 检查账户状态
checkAccountStatus(decryptedAccountNumber);

// 执行存款
BigDecimal newBalance = accountClientService.deposit(decryptedAccountNumber, request.getAmount());

// 记录交易
Transaction transaction = new Transaction();
transaction.setTransactionId(idGenerator.generate());
transaction.setAccountNumber(decryptedAccountNumber);
transaction.setTransactionType(TransactionType.DEPOSIT);
transaction.setStatus(TransactionStatus.COMPLETED);
transaction.setAmount(request.getAmount());
transaction.setCurrency("CNY");
transaction.setDescription(request.getDescription());
transaction.setReference("DEP-" + System.currentTimeMillis());

Transaction saved = transactionRepository.save(transaction);
log.info("Deposit completed: {}", saved.getTransactionId());

return convertToDTO(saved);
}

@Transactional
@PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN') || "
+ "(hasRole('CUSTOMER') && @accountSecurityService.isAccountOwner(authentication, #request.accountNumber))")
public TransactionDTO withdraw(WithdrawalRequest request) {
// 解密账号
String decryptedAccountNumber = encryptionUtil.decrypt(request.getAccountNumber());

// 验证金额
validateAmount(request.getAmount());

// 检查账户状态
checkAccountStatus(decryptedAccountNumber);

// 检查交易限额
checkWithdrawalLimit(decryptedAccountNumber, request.getAmount());

// 执行取款
BigDecimal newBalance = accountClientService.withdraw(decryptedAccountNumber, request.getAmount());

// 记录交易
Transaction transaction = new Transaction();
transaction.setTransactionId(idGenerator.generate());
transaction.setAccountNumber(decryptedAccountNumber);
transaction.setTransactionType(TransactionType.WITHDRAWAL);
transaction.setStatus(TransactionStatus.COMPLETED);
transaction.setAmount(request.getAmount().negate());
transaction.setCurrency("CNY");
transaction.setDescription(request.getDescription());
transaction.setReference("WTH-" + System.currentTimeMillis());

Transaction saved = transactionRepository.save(transaction);
log.info("Withdrawal completed: {}", saved.getTransactionId());

return convertToDTO(saved);
}

@Transactional
@PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN') || "
+ "(hasRole('CUSTOMER') && @accountSecurityService.isAccountOwner(authentication, #request.fromAccountNumber))")
public TransactionDTO transfer(TransferRequest request) {
// 解密账号
String decryptedFromAccount = encryptionUtil.decrypt(request.getFromAccountNumber());
String decryptedToAccount = encryptionUtil.decrypt(request.getToAccountNumber());

// 验证金额
validateAmount(request.getAmount());

// 检查账户状态
checkAccountStatus(decryptedFromAccount);
checkAccountStatus(decryptedToAccount);

// 检查转账限额
checkTransferLimit(decryptedFromAccount, request.getAmount());

// 执行转账
BigDecimal fromNewBalance = accountClientService.withdraw(decryptedFromAccount, request.getAmount());
BigDecimal toNewBalance = accountClientService.deposit(decryptedToAccount, request.getAmount());

// 记录交易(借方)
Transaction debitTransaction = new Transaction();
debitTransaction.setTransactionId(idGenerator.generate());
debitTransaction.setAccountNumber(decryptedFromAccount);
debitTransaction.setCounterpartyAccountNumber(decryptedToAccount);
debitTransaction.setTransactionType(TransactionType.TRANSFER);
debitTransaction.setStatus(TransactionStatus.COMPLETED);
debitTransaction.setAmount(request.getAmount().negate());
debitTransaction.setCurrency("CNY");
debitTransaction.setDescription(request.getDescription());
debitTransaction.setReference("TFR-DEBIT-" + System.currentTimeMillis());

// 记录交易(贷方)
Transaction creditTransaction = new Transaction();
creditTransaction.setTransactionId(idGenerator.generate());
creditTransaction.setAccountNumber(decryptedToAccount);
creditTransaction.setCounterpartyAccountNumber(decryptedFromAccount);
creditTransaction.setTransactionType(TransactionType.TRANSFER);
creditTransaction.setStatus(TransactionStatus.COMPLETED);
creditTransaction.setAmount(request.getAmount());
creditTransaction.setCurrency("CNY");
creditTransaction.setDescription(request.getDescription());
creditTransaction.setReference("TFR-CREDIT-" + System.currentTimeMillis());

transactionRepository.save(debitTransaction);
transactionRepository.save(creditTransaction);
log.info("Transfer completed: {} -> {}", debitTransaction.getTransactionId(), creditTransaction.getTransactionId());

return convertToDTO(debitTransaction);
}

@Transactional
@PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN')")
public List<TransactionDTO> batchTransfer(BatchTransactionRequest request) {
return request.getTransactions().stream()
.map(this::transfer)
.collect(Collectors.toList());
}

@Transactional(readOnly = true)
@PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN') || "
+ "(hasRole('CUSTOMER') && @accountSecurityService.isAccountOwner(authentication, #accountNumber))")
public List<TransactionDTO> getTransactionHistory(String accountNumber, LocalDate startDate, LocalDate endDate) {
String decryptedAccountNumber = encryptionUtil.decrypt(accountNumber);

LocalDateTime start = startDate.atStartOfDay();
LocalDateTime end = endDate.atTime(LocalTime.MAX);

return transactionRepository
.findByAccountNumberAndCreatedAtBetween(decryptedAccountNumber, start, end)
.stream()
.map(this::convertToDTO)
.peek(dto -> dto.setAccountNumber(encryptionUtil.encrypt(dto.getAccountNumber())))
.peek(dto -> {
if (dto.getCounterpartyAccountNumber() != null) {
dto.setCounterpartyAccountNumber(encryptionUtil.encrypt(dto.getCounterpartyAccountNumber()));
}
})
.collect(Collectors.toList());
}

private void validateAmount(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new AccountException(ErrorCode.INVALID_AMOUNT);
}
}

private void checkAccountStatus(String accountNumber) {
AccountDTO account = accountClientService.getAccount(accountNumber);
if (account.getStatus() != AccountStatus.ACTIVE) {
throw new AccountException(ErrorCode.ACCOUNT_NOT_ACTIVE);
}
}

private void checkWithdrawalLimit(String accountNumber, BigDecimal amount) {
AccountDTO account = accountClientService.getAccount(accountNumber);
TransactionLimit limit = limitRepository.findByTransactionTypeAndAccountType(
TransactionType.WITHDRAWAL, account.getAccountType().name())
.orElseThrow(() -> new AccountException(ErrorCode.TRANSACTION_LIMIT_NOT_FOUND));

// 检查单笔限额
if (amount.compareTo(limit.getPerTransactionLimit()) > 0) {
throw new AccountException(ErrorCode.EXCEED_PER_TRANSACTION_LIMIT);
}

// 检查当日累计限额
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
LocalDateTime todayEnd = LocalDate.now().atTime(LocalTime.MAX);

BigDecimal todayTotal = transactionRepository
.findByAccountNumberAndCreatedAtBetween(accountNumber, todayStart, todayEnd)
.stream()
.filter(t -> t.getTransactionType() == TransactionType.WITHDRAWAL)
.map(Transaction::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.abs();

if (todayTotal.add(amount).compareTo(limit.getDailyLimit()) > 0) {
throw new AccountException(ErrorCode.EXCEED_DAILY_LIMIT);
}

// 检查当日交易次数
long todayCount = transactionRepository
.findByAccountNumberAndCreatedAtBetween(accountNumber, todayStart, todayEnd)
.stream()
.filter(t -> t.getTransactionType() == TransactionType.WITHDRAWAL)
.count();

if (todayCount >= limit.getDailyCountLimit()) {
throw new AccountException(ErrorCode.EXCEED_DAILY_COUNT_LIMIT);
}
}

private void checkTransferLimit(String accountNumber, BigDecimal amount) {
AccountDTO account = accountClientService.getAccount(accountNumber);
TransactionLimit limit = limitRepository.findByTransactionTypeAndAccountType(
TransactionType.TRANSFER, account.getAccountType().name())
.orElseThrow(() -> new AccountException(ErrorCode.TRANSACTION_LIMIT_NOT_FOUND));

// 检查单笔限额
if (amount.compareTo(limit.getPerTransactionLimit()) > 0) {
throw new AccountException(ErrorCode.EXCEED_PER_TRANSACTION_LIMIT);
}

// 检查当日累计限额
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
LocalDateTime todayEnd = LocalDate.now().atTime(LocalTime.MAX);

BigDecimal todayTotal = transactionRepository
.findByAccountNumberAndCreatedAtBetween(accountNumber, todayStart, todayEnd)
.stream()
.filter(t -> t.getTransactionType() == TransactionType.TRANSFER)
.map(Transaction::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.abs();

if (todayTotal.add(amount).compareTo(limit.getDailyLimit()) > 0) {
throw new AccountException(ErrorCode.EXCEED_DAILY_LIMIT);
}
}

private TransactionDTO convertToDTO(Transaction transaction) {
TransactionDTO dto = new TransactionDTO();
dto.setTransactionId(transaction.getTransactionId());
dto.setAccountNumber(transaction.getAccountNumber());
dto.setCounterpartyAccountNumber(transaction.getCounterpartyAccountNumber());
dto.setTransactionType(transaction.getTransactionType());
dto.setStatus(transaction.getStatus());
dto.setAmount(transaction.getAmount());
dto.setFee(transaction.getFee());
dto.setCurrency(transaction.getCurrency());
dto.setDescription(transaction.getDescription());
dto.setReference(transaction.getReference());
dto.setCreatedAt(transaction.getCreatedAt());
dto.setUpdatedAt(transaction.getUpdatedAt());
return dto;
}
}

@Service
@RequiredArgsConstructor
public class AccountClientService {
private final RestTemplate restTemplate;
private final EncryptionUtil encryptionUtil;

@Value("${service.account.url}")
private String accountServiceUrl;

public AccountDTO getAccount(String accountNumber) {
try {
String encryptedAccountNumber = encryptionUtil.encrypt(accountNumber);

HttpHeaders headers = new HttpHeaders();
headers.set("X-Internal-Service", "transaction-service");

ResponseEntity<AccountDTO> response = restTemplate.exchange(
accountServiceUrl + "/api/accounts/" + encryptedAccountNumber,
HttpMethod.GET,
new HttpEntity<>(headers),
AccountDTO.class);

AccountDTO account = response.getBody();
if (account != null) {
account.setAccountNumber(accountNumber); // 返回解密后的账号
}
return account;
} catch (HttpClientErrorException.NotFound e) {
throw new AccountException(ErrorCode.ACCOUNT_NOT_FOUND);
} catch (Exception e) {
throw new AccountException(ErrorCode.ACCOUNT_SERVICE_UNAVAILABLE);
}
}

public BigDecimal deposit(String accountNumber, BigDecimal amount) {
try {
String encryptedAccountNumber = encryptionUtil.encrypt(accountNumber);

HttpHeaders headers = new HttpHeaders();
headers.set("X-Internal-Service", "transaction-service");

ResponseEntity<AccountOperationResponse> response = restTemplate.exchange(
accountServiceUrl + "/api/accounts/" + encryptedAccountNumber + "/deposit?amount=" + amount,
HttpMethod.POST,
new HttpEntity<>(headers),
AccountOperationResponse.class);

AccountOperationResponse result = response.getBody();
if (result == null || !result.isSuccess()) {
throw new AccountException(ErrorCode.DEPOSIT_FAILED);
}
return result.getNewBalance();
} catch (Exception e) {
throw new AccountException(ErrorCode.ACCOUNT_SERVICE_UNAVAILABLE);
}
}

public BigDecimal withdraw(String accountNumber, BigDecimal amount) {
try {
String encryptedAccountNumber = encryptionUtil.encrypt(accountNumber);

HttpHeaders headers = new HttpHeaders();
headers.set("X-Internal-Service", "transaction-service");

ResponseEntity<AccountOperationResponse> response = restTemplate.exchange(
accountServiceUrl + "/api/accounts/" + encryptedAccountNumber + "/withdraw?amount=" + amount,
HttpMethod.POST,
new HttpEntity<>(headers),
AccountOperationResponse.class);

AccountOperationResponse result = response.getBody();
if (result == null || !result.isSuccess()) {
throw new AccountException(ErrorCode.WITHDRAWAL_FAILED);
}
return result.getNewBalance();
} catch (Exception e) {
throw new AccountException(ErrorCode.ACCOUNT_SERVICE_UNAVAILABLE);
}
}
}

Repository层

public interface TransactionRepository extends JpaRepository<Transaction, Long> {
Optional<Transaction> findByTransactionId(String transactionId);

List<Transaction> findByAccountNumberAndCreatedAtBetween(
String accountNumber, LocalDateTime startDate, LocalDateTime endDate);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT t FROM Transaction t WHERE t.transactionId = :transactionId")
Optional<Transaction> findByTransactionIdForUpdate(@Param("transactionId") String transactionId);
}

public interface TransactionLimitRepository extends JpaRepository<TransactionLimit, Long> {
Optional<TransactionLimit> findByTransactionTypeAndAccountType(
TransactionType transactionType, String accountType);
}

3. 支付结算

支付订单处理

清算对账

手续费计算

第三方支付对接

# 在原有配置基础上添加
payment:
settlement:
auto-enabled: true
time: "02:00" # 自动结算时间
third-party:
timeout: 5000 # 第三方支付超时时间(ms)
retry-times: 3 # 重试次数

@Entity
@Table(name = "payment_order")
@Data
public class PaymentOrder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String orderNo;

@Column(nullable = false)
private String accountNumber;

@Column(nullable = false)
private String merchantCode;

@Column(nullable = false)
private String merchantOrderNo;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PaymentOrderType orderType;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PaymentOrderStatus status;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal amount;

@Column(precision = 19, scale = 4)
private BigDecimal fee;

@Column(precision = 19, scale = 4)
private BigDecimal settlementAmount;

@Column(nullable = false)
private String currency = "CNY";

@Column
private String description;

@Column
private String callbackUrl;

@Column
private String notifyUrl;

@Column
private String thirdPartyTransactionNo;

@Column
private LocalDateTime paymentTime;

@Column
private LocalDateTime settlementTime;

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;

@Version
private Long version;
}

@Entity
@Table(name = "settlement_record")
@Data
public class SettlementRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String settlementNo;

@Column(nullable = false)
private String merchantCode;

@Column(nullable = false)
private LocalDate settlementDate;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal totalAmount;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal totalFee;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal netAmount;

@Column(nullable = false)
private Integer totalCount;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private SettlementStatus status;

@Column
private String bankTransactionNo;

@Column
private LocalDateTime completedTime;

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;
}

@Entity
@Table(name = "fee_config")
@Data
public class FeeConfig {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String merchantCode;

@Column(nullable = false)
private String paymentType;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private FeeCalculateMethod calculateMethod;

@Column(precision = 19, scale = 4)
private BigDecimal fixedFee;

@Column(precision = 5, scale = 4)
private BigDecimal rate;

@Column(precision = 19, scale = 4)
private BigDecimal minFee;

@Column(precision = 19, scale = 4)
private BigDecimal maxFee;

@Column(nullable = false)
private Boolean active = true;
}

public enum PaymentOrderType {
WECHAT_PAY, // 微信支付
ALI_PAY, // 支付宝
UNION_PAY, // 银联
QUICK_PAY, // 快捷支付
BANK_TRANSFER // 银行转账
}

public enum PaymentOrderStatus {
CREATED, // 已创建
PROCESSING, // 处理中
SUCCESS, // 支付成功
FAILED, // 支付失败
REFUNDED, // 已退款
CLOSED // 已关闭
}

public enum SettlementStatus {
PENDING, // 待结算
PROCESSING, // 结算中
COMPLETED, // 结算完成
FAILED // 结算失败
}

public enum FeeCalculateMethod {
FIXED, // 固定费用
PERCENTAGE, // 百分比
TIERED // 阶梯费率
}

@Data
public class PaymentRequestDTO {
@NotBlank
private String accountNumber;

@NotBlank
private String merchantCode;

@NotBlank
private String merchantOrderNo;

@NotNull
private PaymentOrderType orderType;

@NotNull
@Positive
private BigDecimal amount;

@NotBlank
private String currency;

private String description;

private String callbackUrl;

private String notifyUrl;
}

@Data
public class PaymentResponseDTO {
private String orderNo;
private String merchantOrderNo;
private PaymentOrderStatus status;
private BigDecimal amount;
private BigDecimal fee;
private BigDecimal settlementAmount;
private String currency;
private String paymentUrl; // 用于前端跳转支付
private LocalDateTime createdAt;
}

@Data
public class SettlementRequestDTO {
@NotBlank
private String merchantCode;

private LocalDate settlementDate;
}

@Data
public class SettlementResponseDTO {
private String settlementNo;
private String merchantCode;
private LocalDate settlementDate;
private BigDecimal totalAmount;
private BigDecimal totalFee;
private BigDecimal netAmount;
private Integer totalCount;
private SettlementStatus status;
private LocalDateTime completedTime;
}

@Data
public class ThirdPartyPaymentRequest {
private String orderNo;
private BigDecimal amount;
private String currency;
private String accountNumber;
private String merchantCode;
private String paymentType;
}

@Data
public class ThirdPartyPaymentResponse {
private boolean success;
private String transactionNo;
private String paymentUrl;
private String errorCode;
private String errorMessage;
}

工具类

@Component
public class OrderNoGenerator {
private static final String BANK_CODE = "888";
private final AtomicLong sequence = new AtomicLong(1);

public String generate() {
long timestamp = Instant.now().toEpochMilli();
long seq = sequence.getAndIncrement();
return String.format("%s-PAY-%d-%06d", BANK_CODE, timestamp, seq);
}
}

@Component
public class SettlementNoGenerator {
private static final String BANK_CODE = "888";
private final AtomicLong sequence = new AtomicLong(1);

public String generate() {
String dateStr = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
long seq = sequence.getAndIncrement();
return String.format("%s-STL-%s-%06d", BANK_CODE, dateStr, seq);
}
}

Controller层

@RestController
@RequestMapping("/api/payments")
@RequiredArgsConstructor
public class PaymentController {
private final PaymentService paymentService;
private final SettlementService settlementService;

@PostMapping
public PaymentResponseDTO createPayment(@Valid @RequestBody PaymentRequestDTO request) {
return paymentService.createPayment(request);
}

@GetMapping("/{orderNo}")
public PaymentResponseDTO queryPayment(@PathVariable String orderNo) {
return paymentService.queryPayment(orderNo);
}

@GetMapping
public PaymentResponseDTO queryPaymentByMerchant(
@RequestParam String merchantCode,
@RequestParam String merchantOrderNo) {
return paymentService.queryPaymentByMerchant(merchantCode, merchantOrderNo);
}

@PostMapping("/settlements")
public SettlementResponseDTO createSettlement(@Valid @RequestBody SettlementRequestDTO request) {
return settlementService.createSettlement(request);
}

@GetMapping("/settlements/{settlementNo}")
public SettlementResponseDTO querySettlement(@PathVariable String settlementNo) {
return settlementService.querySettlement(settlementNo);
}
}

Service层

@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentService {
private final PaymentOrderRepository paymentOrderRepository;
private final FeeConfigRepository feeConfigRepository;
private final TransactionService transactionService;
private final ThirdPartyPaymentGateway paymentGateway;
private final OrderNoGenerator orderNoGenerator;
private final EncryptionUtil encryptionUtil;

@Transactional
public PaymentResponseDTO createPayment(PaymentRequestDTO request) {
// 解密账号
String decryptedAccountNumber = encryptionUtil.decrypt(request.getAccountNumber());

// 检查是否已存在相同商户订单
Optional<PaymentOrder> existingOrder = paymentOrderRepository
.findByMerchantCodeAndMerchantOrderNo(request.getMerchantCode(), request.getMerchantOrderNo());
if (existingOrder.isPresent()) {
throw new AccountException(ErrorCode.DUPLICATE_MERCHANT_ORDER);
}

// 计算手续费
BigDecimal fee = calculateFee(request.getMerchantCode(), request.getOrderType().name(), request.getAmount());
BigDecimal settlementAmount = request.getAmount().subtract(fee);

// 创建支付订单
PaymentOrder order = new PaymentOrder();
order.setOrderNo(orderNoGenerator.generate());
order.setAccountNumber(decryptedAccountNumber);
order.setMerchantCode(request.getMerchantCode());
order.setMerchantOrderNo(request.getMerchantOrderNo());
order.setOrderType(request.getOrderType());
order.setStatus(PaymentOrderStatus.CREATED);
order.setAmount(request.getAmount());
order.setFee(fee);
order.setSettlementAmount(settlementAmount);
order.setCurrency(request.getCurrency());
order.setDescription(request.getDescription());
order.setCallbackUrl(request.getCallbackUrl());
order.setNotifyUrl(request.getNotifyUrl());

PaymentOrder savedOrder = paymentOrderRepository.save(order);
log.info("Payment order created: {}", savedOrder.getOrderNo());

// 异步处理支付
processPaymentAsync(savedOrder.getOrderNo());

return convertToPaymentResponse(savedOrder);
}

@Async
public void processPaymentAsync(String orderNo) {
try {
PaymentOrder order = paymentOrderRepository.findByOrderNoForUpdate(orderNo)
.orElseThrow(() -> new AccountException(ErrorCode.ORDER_NOT_FOUND));

if (order.getStatus() != PaymentOrderStatus.CREATED) {
return;
}

order.setStatus(PaymentOrderStatus.PROCESSING);
paymentOrderRepository.save(order);

// 调用第三方支付
ThirdPartyPaymentRequest paymentRequest = new ThirdPartyPaymentRequest();
paymentRequest.setOrderNo(order.getOrderNo());
paymentRequest.setAmount(order.getAmount());
paymentRequest.setCurrency(order.getCurrency());
paymentRequest.setAccountNumber(order.getAccountNumber());
paymentRequest.setMerchantCode(order.getMerchantCode());
paymentRequest.setPaymentType(order.getOrderType().name());

ThirdPartyPaymentResponse paymentResponse = paymentGateway.processPayment(paymentRequest);

if (paymentResponse.isSuccess()) {
order.setStatus(PaymentOrderStatus.SUCCESS);
order.setThirdPartyTransactionNo(paymentResponse.getTransactionNo());
order.setPaymentTime(LocalDateTime.now());

// 记录交易
transactionService.withdraw(
order.getAccountNumber(),
order.getAmount(),
"Payment for order: " + order.getOrderNo());
} else {
order.setStatus(PaymentOrderStatus.FAILED);
log.error("Payment failed for order {}: {}", orderNo, paymentResponse.getErrorMessage());
}

paymentOrderRepository.save(order);

// 回调商户
if (order.getCallbackUrl() != null) {
notifyMerchant(order);
}

} catch (Exception e) {
log.error("Error processing payment for order: " + orderNo, e);
paymentOrderRepository.findByOrderNo(orderNo).ifPresent(order -> {
order.setStatus(PaymentOrderStatus.FAILED);
paymentOrderRepository.save(order);
});
}
}

@Transactional(readOnly = true)
public PaymentResponseDTO queryPayment(String orderNo) {
PaymentOrder order = paymentOrderRepository.findByOrderNo(orderNo)
.orElseThrow(() -> new AccountException(ErrorCode.ORDER_NOT_FOUND));

return convertToPaymentResponse(order);
}

@Transactional(readOnly = true)
public PaymentResponseDTO queryPaymentByMerchant(String merchantCode, String merchantOrderNo) {
PaymentOrder order = paymentOrderRepository.findByMerchantCodeAndMerchantOrderNo(merchantCode, merchantOrderNo)
.orElseThrow(() -> new AccountException(ErrorCode.ORDER_NOT_FOUND));

return convertToPaymentResponse(order);
}

private BigDecimal calculateFee(String merchantCode, String paymentType, BigDecimal amount) {
FeeConfig feeConfig = feeConfigRepository.findByMerchantCodeAndPaymentType(merchantCode, paymentType)
.orElseThrow(() -> new AccountException(ErrorCode.FEE_CONFIG_NOT_FOUND));

switch (feeConfig.getCalculateMethod()) {
case FIXED:
return feeConfig.getFixedFee();
case PERCENTAGE:
BigDecimal fee = amount.multiply(feeConfig.getRate());
if (feeConfig.getMinFee() != null && fee.compareTo(feeConfig.getMinFee()) < 0) {
return feeConfig.getMinFee();
}
if (feeConfig.getMaxFee() != null && fee.compareTo(feeConfig.getMaxFee()) > 0) {
return feeConfig.getMaxFee();
}
return fee;
case TIERED:
// 实现阶梯费率计算逻辑
return feeConfig.getFixedFee(); // 简化处理
default:
return BigDecimal.ZERO;
}
}

private void notifyMerchant(PaymentOrder order) {
// 实现回调商户逻辑
// 通常使用HTTP调用商户的callbackUrl或notifyUrl
log.info("Notifying merchant for order: {}", order.getOrderNo());
}

private PaymentResponseDTO convertToPaymentResponse(PaymentOrder order) {
PaymentResponseDTO response = new PaymentResponseDTO();
response.setOrderNo(order.getOrderNo());
response.setMerchantOrderNo(order.getMerchantOrderNo());
response.setStatus(order.getStatus());
response.setAmount(order.getAmount());
response.setFee(order.getFee());
response.setSettlementAmount(order.getSettlementAmount());
response.setCurrency(order.getCurrency());
response.setCreatedAt(order.getCreatedAt());

// 加密账号
response.setAccountNumber(encryptionUtil.encrypt(order.getAccountNumber()));

return response;
}
}

/**
结算服务
*/

@Service
@RequiredArgsConstructor
@Slf4j
public class SettlementService {
private final PaymentOrderRepository paymentOrderRepository;
private final SettlementRecordRepository settlementRepository;
private final TransactionService transactionService;
private final SettlementNoGenerator settlementNoGenerator;

@Transactional
public SettlementResponseDTO createSettlement(SettlementRequestDTO request) {
LocalDate settlementDate = request.getSettlementDate() != null ?
request.getSettlementDate() : LocalDate.now().minusDays(1);

// 检查是否已有结算记录
Optional<SettlementRecord> existingSettlement = settlementRepository
.findByMerchantCodeAndSettlementDate(request.getMerchantCode(), settlementDate);
if (existingSettlement.isPresent()) {
throw new AccountException(ErrorCode.SETTLEMENT_ALREADY_EXISTS);
}

// 查询待结算的支付订单
List<PaymentOrder> orders = paymentOrderRepository
.findByMerchantCodeAndStatusAndSettlementTimeIsNull(
request.getMerchantCode(),
PaymentOrderStatus.SUCCESS);

if (orders.isEmpty()) {
throw new AccountException(ErrorCode.NO_ORDERS_TO_SETTLE);
}

// 计算结算金额
BigDecimal totalAmount = orders.stream()
.map(PaymentOrder::getSettlementAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);

BigDecimal totalFee = orders.stream()
.map(PaymentOrder::getFee)
.reduce(BigDecimal.ZERO, BigDecimal::add);

// 创建结算记录
SettlementRecord settlement = new SettlementRecord();
settlement.setSettlementNo(settlementNoGenerator.generate());
settlement.setMerchantCode(request.getMerchantCode());
settlement.setSettlementDate(settlementDate);
settlement.setTotalAmount(totalAmount);
settlement.setTotalFee(totalFee);
settlement.setNetAmount(totalAmount);
settlement.setTotalCount(orders.size());
settlement.setStatus(SettlementStatus.PENDING);

SettlementRecord savedSettlement = settlementRepository.save(settlement);
log.info("Settlement record created: {}", savedSettlement.getSettlementNo());

// 异步处理结算
processSettlementAsync(savedSettlement.getSettlementNo());

return convertToSettlementResponse(savedSettlement);
}

@Async
public void processSettlementAsync(String settlementNo) {
try {
SettlementRecord settlement = settlementRepository.findBySettlementNo(settlementNo)
.orElseThrow(() -> new AccountException(ErrorCode.SETTLEMENT_NOT_FOUND));

if (settlement.getStatus() != SettlementStatus.PENDING) {
return;
}

settlement.setStatus(SettlementStatus.PROCESSING);
settlementRepository.save(settlement);

// 执行资金划拨
transactionService.deposit(
getMerchantAccount(settlement.getMerchantCode()),
settlement.getNetAmount(),
"Settlement for " + settlement.getSettlementDate());

// 更新支付订单结算状态
List<PaymentOrder> orders = paymentOrderRepository
.findByMerchantCodeAndStatusAndSettlementTimeIsNull(
settlement.getMerchantCode(),
PaymentOrderStatus.SUCCESS);

orders.forEach(order -> {
order.setSettlementTime(LocalDateTime.now());
paymentOrderRepository.save(order);
});

settlement.setStatus(SettlementStatus.COMPLETED);
settlement.setCompletedTime(LocalDateTime.now());
settlementRepository.save(settlement);

log.info("Settlement completed: {}", settlementNo);

} catch (Exception e) {
log.error("Error processing settlement: " + settlementNo, e);
settlementRepository.findBySettlementNo(settlementNo).ifPresent(s -> {
s.setStatus(SettlementStatus.FAILED);
settlementRepository.save(s);
});
}
}

@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void autoSettlement() {
LocalDate settlementDate = LocalDate.now().minusDays(1);
log.info("Starting auto settlement for date: {}", settlementDate);

// 获取所有需要结算的商户
List<String> merchantCodes = paymentOrderRepository
.findDistinctMerchantCodeByStatusAndSettlementTimeIsNull(PaymentOrderStatus.SUCCESS);

merchantCodes.forEach(merchantCode -> {
try {
SettlementRequestDTO request = new SettlementRequestDTO();
request.setMerchantCode(merchantCode);
request.setSettlementDate(settlementDate);
createSettlement(request);
} catch (Exception e) {
log.error("Auto settlement failed for merchant: " + merchantCode, e);
}
});
}

private String getMerchantAccount(String merchantCode) {
// 实际项目中应根据商户编码查询商户的结算账户
return "MERCHANT_" + merchantCode;
}

private SettlementResponseDTO convertToSettlementResponse(SettlementRecord settlement) {
SettlementResponseDTO response = new SettlementResponseDTO();
response.setSettlementNo(settlement.getSettlementNo());
response.setMerchantCode(settlement.getMerchantCode());
response.setSettlementDate(settlement.getSettlementDate());
response.setTotalAmount(settlement.getTotalAmount());
response.setTotalFee(settlement.getTotalFee());
response.setNetAmount(settlement.getNetAmount());
response.setTotalCount(settlement.getTotalCount());
response.setStatus(settlement.getStatus());
response.setCompletedTime(settlement.getCompletedTime());
return response;
}
}

/**
第三方支付对接
*/

@Component
public class ThirdPartyPaymentGateway {
public ThirdPartyPaymentResponse processPayment(ThirdPartyPaymentRequest request) {
// 实际项目中这里会调用第三方支付平台的API
// 以下是模拟实现

ThirdPartyPaymentResponse response = new ThirdPartyPaymentResponse();

try {
// 模拟支付处理
Thread.sleep(500);

// 模拟90%成功率
if (Math.random() > 0.1) {
response.setSuccess(true);
response.setTransactionNo("TP" + System.currentTimeMillis());
response.setPaymentUrl("https://payment-gateway.com/pay/" + request.getOrderNo());
} else {
response.setSuccess(false);
response.setErrorCode("PAYMENT_FAILED");
response.setErrorMessage("Payment processing failed");
}
} catch (Exception e) {
response.setSuccess(false);
response.setErrorCode("SYSTEM_ERROR");
response.setErrorMessage(e.getMessage());
}

return response;
}
}

Repository层

public interface PaymentOrderRepository extends JpaRepository<PaymentOrder, Long> {
Optional<PaymentOrder> findByOrderNo(String orderNo);

Optional<PaymentOrder> findByMerchantCodeAndMerchantOrderNo(String merchantCode, String merchantOrderNo);

List<PaymentOrder> findByMerchantCodeAndStatusAndSettlementTimeIsNull(String merchantCode, PaymentOrderStatus status);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM PaymentOrder p WHERE p.orderNo = :orderNo")
Optional<PaymentOrder> findByOrderNoForUpdate(@Param("orderNo") String orderNo);
}

public interface SettlementRecordRepository extends JpaRepository<SettlementRecord, Long> {
Optional<SettlementRecord> findBySettlementNo(String settlementNo);

Optional<SettlementRecord> findByMerchantCodeAndSettlementDate(String merchantCode, LocalDate settlementDate);

List<SettlementRecord> findBySettlementDateAndStatus(LocalDate settlementDate, SettlementStatus status);
}

public interface FeeConfigRepository extends JpaRepository<FeeConfig, Long> {
Optional<FeeConfig> findByMerchantCodeAndPaymentType(String merchantCode, String paymentType);

List<FeeConfig> findByMerchantCode(String merchantCode);
}

4. 贷款管理

贷款申请审批

贷款发放

还款计划

逾期管理

利率调整

@Entity
@Table(name = "loan_application")
@Data
public class LoanApplication {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String applicationNo;

@Column(nullable = false)
private Long customerId;

@Column(nullable = false)
private String accountNumber;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private LoanType loanType;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal amount;

@Column(nullable = false)
private Integer term; // in months

@Column(nullable = false, precision = 5, scale = 4)
private BigDecimal interestRate;

@Column(nullable = false)
private String purpose;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private LoanStatus status;

@Column
private Long approvedBy;

@Column
private LocalDateTime approvedAt;

@Column
private String rejectionReason;

@Column
private LocalDate disbursementDate;

@Column
private LocalDate maturityDate;

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;

@Version
private Long version;
}

@Entity
@Table(name = "loan_account")
@Data
public class LoanAccount {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String loanAccountNo;

@Column(nullable = false)
private String applicationNo;

@Column(nullable = false)
private Long customerId;

@Column(nullable = false)
private String accountNumber;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal originalAmount;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal outstandingAmount;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal interestAccrued;

@Column(nullable = false, precision = 5, scale = 4)
private BigDecimal interestRate;

@Column(nullable = false)
private LocalDate startDate;

@Column(nullable = false)
private LocalDate maturityDate;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private LoanAccountStatus status;

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;

@Version
private Long version;
}

@Entity
@Table(name = "repayment_schedule")
@Data
public class RepaymentSchedule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String loanAccountNo;

@Column(nullable = false)
private Integer installmentNo;

@Column(nullable = false)
private LocalDate dueDate;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal principalAmount;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal interestAmount;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal totalAmount;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal outstandingPrincipal;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private RepaymentStatus status;

@Column
private LocalDate paidDate;

@Column(precision = 19, scale = 4)
private BigDecimal paidAmount;

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;
}

@Entity
@Table(name = "loan_product")
@Data
public class LoanProduct {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String productCode;

@Column(nullable = false)
private String productName;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private LoanType loanType;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal minAmount;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal maxAmount;

@Column(nullable = false)
private Integer minTerm; // in months

@Column(nullable = false)
private Integer maxTerm; // in months

@Column(nullable = false, precision = 5, scale = 4)
private BigDecimal baseInterestRate;

@Column(nullable = false)
private Boolean active = true;
}

public enum LoanType {
PERSONAL_LOAN, // 个人贷款
MORTGAGE_LOAN, // 抵押贷款
AUTO_LOAN, // 汽车贷款
BUSINESS_LOAN, // 商业贷款
CREDIT_LINE // 信用额度
}

public enum LoanStatus {
DRAFT, // 草稿
PENDING, // 待审批
APPROVED, // 已批准
REJECTED, // 已拒绝
DISBURSED, // 已发放
CLOSED // 已关闭
}

public enum LoanAccountStatus {
ACTIVE, // 活跃
DELINQUENT, // 逾期
PAID_OFF, // 已还清
WRITTEN_OFF, // 已核销
DEFAULTED // 违约
}

public enum RepaymentStatus {
PENDING, // 待还款
PAID, // 已还款
PARTIALLY_PAID, // 部分还款
OVERDUE, // 逾期
WAIVED // 已豁免
}

@Data
public class LoanApplicationDTO {
private String applicationNo;
private Long customerId;
private String accountNumber;
private LoanType loanType;
private BigDecimal amount;
private Integer term;
private BigDecimal interestRate;
private String purpose;
private LoanStatus status;
private Long approvedBy;
private LocalDateTime approvedAt;
private String rejectionReason;
private LocalDate disbursementDate;
private LocalDate maturityDate;
private LocalDateTime createdAt;
}

@Data
public class LoanApplicationRequest {
@NotNull
private Long customerId;

@NotBlank
private String accountNumber;

@NotNull
private LoanType loanType;

@NotNull
@DecimalMin("1000.00")
private BigDecimal amount;

@NotNull
@Min(1)
@Max(360)
private Integer term;

@NotBlank
@Size(min = 10, max = 500)
private String purpose;
}

@Data
public class LoanApprovalRequest {
@NotNull
private Boolean approved;

private String comments;
}

@Data
public class LoanAccountDTO {
private String loanAccountNo;
private String applicationNo;
private Long customerId;
private String accountNumber;
private BigDecimal originalAmount;
private BigDecimal outstandingAmount;
private BigDecimal interestAccrued;
private BigDecimal interestRate;
private LocalDate startDate;
private LocalDate maturityDate;
private LoanAccountStatus status;
}

@Data
public class RepaymentDTO {
private Long id;
private String loanAccountNo;
private Integer installmentNo;
private LocalDate dueDate;
private BigDecimal principalAmount;
private BigDecimal interestAmount;
private BigDecimal totalAmount;
private BigDecimal outstandingPrincipal;
private RepaymentStatus status;
private LocalDate paidDate;
private BigDecimal paidAmount;
}

@Data
public class RepaymentRequest {
@NotNull
@DecimalMin("0.01")
private BigDecimal amount;

@NotBlank
private String transactionReference;
}

工具类

@Component
public class ApplicationNoGenerator {
private static final String BANK_CODE = "888";
private final AtomicLong sequence = new AtomicLong(1);

public String generate() {
long timestamp = Instant.now().toEpochMilli();
long seq = sequence.getAndIncrement();
return String.format("%s-LN-%d-%06d", BANK_CODE, timestamp, seq);
}
}

@Component
public class LoanAccountNoGenerator {
private static final String BANK_CODE = "888";
private final AtomicLong sequence = new AtomicLong(1);

public String generate() {
String dateStr = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
long seq = sequence.getAndIncrement();
return String.format("%s-LA-%s-%06d", BANK_CODE, dateStr, seq);
}
}

Controller层

@RestController
@RequestMapping("/api/loan-applications")
@RequiredArgsConstructor
public class LoanApplicationController {
private final LoanApplicationService applicationService;

@PostMapping
public LoanApplicationDTO createApplication(@Valid @RequestBody LoanApplicationRequest request) {
return applicationService.createApplication(request);
}

@PutMapping("/{applicationNo}/approval")
@PreAuthorize("hasAnyRole('LOAN_OFFICER', 'MANAGER', 'ADMIN')")
public LoanApplicationDTO approveApplication(
@PathVariable String applicationNo,
@Valid @RequestBody LoanApprovalRequest request) {
return applicationService.approveApplication(applicationNo, request);
}

@GetMapping("/customer/{customerId}")
public List<LoanApplicationDTO> getCustomerApplications(@PathVariable Long customerId) {
return applicationService.getCustomerApplications(customerId);
}

@GetMapping
@PreAuthorize("hasAnyRole('LOAN_OFFICER', 'MANAGER', 'ADMIN')")
public List<LoanApplicationDTO> getApplicationsByStatus(@RequestParam String status) {
return applicationService.getApplicationsByStatus(LoanStatus.valueOf(status));
}
}

@RestController
@RequestMapping("/api/loan-accounts")
@RequiredArgsConstructor
public class LoanAccountController {
private final LoanAccountService loanAccountService;

@GetMapping("/{loanAccountNo}")
public LoanAccountDTO getLoanAccount(@PathVariable String loanAccountNo) {
return loanAccountService.getLoanAccount(loanAccountNo);
}

@GetMapping("/customer/{customerId}")
public List<LoanAccountDTO> getCustomerLoanAccounts(@PathVariable Long customerId) {
return loanAccountService.getCustomerLoanAccounts(customerId);
}

@GetMapping("/{loanAccountNo}/repayments")
public List<RepaymentDTO> getRepaymentSchedule(@PathVariable String loanAccountNo) {
return loanAccountService.getRepaymentSchedule(loanAccountNo);
}

@PostMapping("/{loanAccountNo}/repayments")
public RepaymentDTO makeRepayment(
@PathVariable String loanAccountNo,
@Valid @RequestBody RepaymentRequest request) {
return loanAccountService.makeRepayment(loanAccountNo, request);
}
}

Service层

@Service
@RequiredArgsConstructor
@Slf4j
public class LoanApplicationService {
private final LoanApplicationRepository applicationRepository;
private final LoanProductRepository productRepository;
private final ApplicationNoGenerator applicationNoGenerator;
private final EncryptionUtil encryptionUtil;

@Transactional
public LoanApplicationDTO createApplication(LoanApplicationRequest request) {
// 解密账号
String decryptedAccountNumber = encryptionUtil.decrypt(request.getAccountNumber());

// 验证贷款产品
LoanProduct product = productRepository.findByLoanType(request.getLoanType())
.orElseThrow(() -> new AccountException(ErrorCode.LOAN_PRODUCT_NOT_FOUND));

// 验证贷款金额和期限
validateLoanAmountAndTerm(request.getAmount(), request.getTerm(), product);

// 计算利率 (简化处理,实际业务中可能有更复杂的利率计算逻辑)
BigDecimal interestRate = calculateInterestRate(request.getLoanType(), request.getAmount(), request.getTerm());

// 创建贷款申请
LoanApplication application = new LoanApplication();
application.setApplicationNo(applicationNoGenerator.generate());
application.setCustomerId(request.getCustomerId());
application.setAccountNumber(decryptedAccountNumber);
application.setLoanType(request.getLoanType());
application.setAmount(request.getAmount());
application.setTerm(request.getTerm());
application.setInterestRate(interestRate);
application.setPurpose(request.getPurpose());
application.setStatus(LoanStatus.PENDING);

LoanApplication saved = applicationRepository.save(application);
log.info("Loan application created: {}", saved.getApplicationNo());

return convertToDTO(saved);
}

@Transactional
@PreAuthorize("hasAnyRole('LOAN_OFFICER', 'MANAGER', 'ADMIN')")
public LoanApplicationDTO approveApplication(String applicationNo, LoanApprovalRequest request) {
LoanApplication application = applicationRepository.findByApplicationNo(applicationNo)
.orElseThrow(() -> new AccountException(ErrorCode.APPLICATION_NOT_FOUND));

if (application.getStatus() != LoanStatus.PENDING) {
throw new AccountException(ErrorCode.APPLICATION_NOT_PENDING);
}

if (request.getApproved()) {
application.setStatus(LoanStatus.APPROVED);
application.setApprovedBy(getCurrentUserId());
application.setApprovedAt(LocalDateTime.now());
application.setMaturityDate(calculateMaturityDate(application.getCreatedAt().toLocalDate(), application.getTerm()));

// 设置预计发放日期(3个工作日后)
application.setDisbursementDate(calculateDisbursementDate(LocalDate.now()));
} else {
application.setStatus(LoanStatus.REJECTED);
application.setRejectionReason(request.getComments());
}

LoanApplication saved = applicationRepository.save(application);
log.info("Loan application {}: {}", request.getApproved() ? "approved" : "rejected", applicationNo);

return convertToDTO(saved);
}

@Transactional(readOnly = true)
public List<LoanApplicationDTO> getCustomerApplications(Long customerId) {
return applicationRepository.findByCustomerId(customerId)
.stream()
.map(this::convertToDTO)
.peek(dto -> dto.setAccountNumber(encryptionUtil.encrypt(dto.getAccountNumber())))
.collect(Collectors.toList());
}

@Transactional(readOnly = true)
@PreAuthorize("hasAnyRole('LOAN_OFFICER', 'MANAGER', 'ADMIN')")
public List<LoanApplicationDTO> getApplicationsByStatus(LoanStatus status) {
return applicationRepository.findByStatus(status)
.stream()
.map(this::convertToDTO)
.peek(dto -> dto.setAccountNumber(encryptionUtil.encrypt(dto.getAccountNumber())))
.collect(Collectors.toList());
}

@Scheduled(cron = "0 0 9 * * ?") // 每天上午9点执行
public void processApprovedLoans() {
LocalDate today = LocalDate.now();
List<LoanApplication> applications = applicationRepository
.findPendingDisbursement(LoanStatus.APPROVED, today);

applications.forEach(application -> {
try {
disburseLoan(application.getApplicationNo());
} catch (Exception e) {
log.error("Failed to disburse loan: " + application.getApplicationNo(), e);
}
});
}

@Transactional
public void disburseLoan(String applicationNo) {
LoanApplication application = applicationRepository.findByApplicationNo(applicationNo)
.orElseThrow(() -> new AccountException(ErrorCode.APPLICATION_NOT_FOUND));

if (application.getStatus() != LoanStatus.APPROVED) {
throw new AccountException(ErrorCode.APPLICATION_NOT_APPROVED);
}

if (application.getDisbursementDate().isAfter(LocalDate.now())) {
throw new AccountException(ErrorCode.DISBURSEMENT_DATE_NOT_REACHED);
}

// 创建贷款账户
LoanAccountDTO loanAccount = createLoanAccount(application);

// 标记贷款申请为已发放
application.setStatus(LoanStatus.DISBURSED);
applicationRepository.save(application);

log.info("Loan disbursed: {}", applicationNo);
}

private LoanAccountDTO createLoanAccount(LoanApplication application) {
// 实际实现会调用LoanAccountService创建贷款账户
// 这里简化处理
return new LoanAccountDTO();
}

private void validateLoanAmountAndTerm(BigDecimal amount, Integer term, LoanProduct product) {
if (amount.compareTo(product.getMinAmount()) < 0 || amount.compareTo(product.getMaxAmount()) > 0) {
throw new AccountException(ErrorCode.INVALID_LOAN_AMOUNT);
}

if (term < product.getMinTerm() || term > product.getMaxTerm()) {
throw new AccountException(ErrorCode.INVALID_LOAN_TERM);
}
}

private BigDecimal calculateInterestRate(LoanType loanType, BigDecimal amount, Integer term) {
// 简化处理,实际业务中可能有更复杂的利率计算逻辑
return BigDecimal.valueOf(0.08); // 8%
}

private LocalDate calculateMaturityDate(LocalDate startDate, Integer termMonths) {
return startDate.plusMonths(termMonths);
}

private LocalDate calculateDisbursementDate(LocalDate today) {
// 简化处理,实际业务中可能需要考虑工作日
return today.plusDays(3);
}

private Long getCurrentUserId() {
// 从安全上下文中获取当前用户ID
return 1L; // 简化处理
}

private LoanApplicationDTO convertToDTO(LoanApplication application) {
LoanApplicationDTO dto = new LoanApplicationDTO();
dto.setApplicationNo(application.getApplicationNo());
dto.setCustomerId(application.getCustomerId());
dto.setAccountNumber(application.getAccountNumber());
dto.setLoanType(application.getLoanType());
dto.setAmount(application.getAmount());
dto.setTerm(application.getTerm());
dto.setInterestRate(application.getInterestRate());
dto.setPurpose(application.getPurpose());
dto.setStatus(application.getStatus());
dto.setApprovedBy(application.getApprovedBy());
dto.setApprovedAt(application.getApprovedAt());
dto.setRejectionReason(application.getRejectionReason());
dto.setDisbursementDate(application.getDisbursementDate());
dto.setMaturityDate(application.getMaturityDate());
dto.setCreatedAt(application.getCreatedAt());
return dto;
}
}

/**
贷款账户服务
*/

@Service
@RequiredArgsConstructor
@Slf4j
public class LoanAccountService {
private final LoanAccountRepository loanAccountRepository;
private final LoanApplicationRepository applicationRepository;
private final RepaymentScheduleRepository repaymentRepository;
private final TransactionService transactionService;
private final LoanAccountNoGenerator accountNoGenerator;

@Transactional
public LoanAccountDTO createLoanAccount(LoanApplication application) {
// 生成还款计划
List<RepaymentSchedule> repaymentSchedules = generateRepaymentSchedule(application);

// 创建贷款账户
LoanAccount loanAccount = new LoanAccount();
loanAccount.setLoanAccountNo(accountNoGenerator.generate());
loanAccount.setApplicationNo(application.getApplicationNo());
loanAccount.setCustomerId(application.getCustomerId());
loanAccount.setAccountNumber(application.getAccountNumber());
loanAccount.setOriginalAmount(application.getAmount());
loanAccount.setOutstandingAmount(application.getAmount());
loanAccount.setInterestAccrued(BigDecimal.ZERO);
loanAccount.setInterestRate(application.getInterestRate());
loanAccount.setStartDate(LocalDate.now());
loanAccount.setMaturityDate(application.getMaturityDate());
loanAccount.setStatus(LoanAccountStatus.ACTIVE);

LoanAccount savedAccount = loanAccountRepository.save(loanAccount);

// 保存还款计划
repaymentSchedules.forEach(schedule -> schedule.setLoanAccountNo(savedAccount.getLoanAccountNo()));
repaymentRepository.saveAll(repaymentSchedules);

// 发放贷款资金
transactionService.deposit(
application.getAccountNumber(),
application.getAmount(),
"Loan disbursement for " + savedAccount.getLoanAccountNo());

log.info("Loan account created: {}", savedAccount.getLoanAccountNo());

return convertToDTO(savedAccount);
}

@Transactional
public RepaymentDTO makeRepayment(String loanAccountNo, RepaymentRequest request) {
LoanAccount loanAccount = loanAccountRepository.findByLoanAccountNoForUpdate(loanAccountNo)
.orElseThrow(() -> new AccountException(ErrorCode.LOAN_ACCOUNT_NOT_FOUND));

if (loanAccount.getStatus() != LoanAccountStatus.ACTIVE &&
loanAccount.getStatus() != LoanAccountStatus.DELINQUENT) {
throw new AccountException(ErrorCode.LOAN_ACCOUNT_NOT_ACTIVE);
}

// 查找到期的还款计划
List<RepaymentSchedule> dueInstallments = repaymentRepository
.findDueInstallments(
loanAccountNo,
LocalDate.now(),
List.of(RepaymentStatus.PENDING, RepaymentStatus.OVERDUE));

if (dueInstallments.isEmpty()) {
throw new AccountException(ErrorCode.NO_DUE_INSTALLMENTS);
}

// 处理还款
BigDecimal remainingAmount = request.getAmount();
for (RepaymentSchedule installment : dueInstallments) {
if (remainingAmount.compareTo(BigDecimal.ZERO) <= 0) {
break;
}

BigDecimal amountToPay = installment.getTotalAmount().subtract(installment.getPaidAmount() != null ?
installment.getPaidAmount() : BigDecimal.ZERO);
BigDecimal paymentAmount = remainingAmount.compareTo(amountToPay) >= 0 ?
amountToPay : remainingAmount;

// 记录还款
installment.setPaidAmount((installment.getPaidAmount() != null ?
installment.getPaidAmount() : BigDecimal.ZERO).add(paymentAmount));

if (installment.getPaidAmount().compareTo(installment.getTotalAmount()) >= 0) {
installment.setStatus(RepaymentStatus.PAID);
installment.setPaidDate(LocalDate.now());
} else {
installment.setStatus(RepaymentStatus.PARTIALLY_PAID);
}

repaymentRepository.save(installment);
remainingAmount = remainingAmount.subtract(paymentAmount);

// 更新贷款账户余额
BigDecimal principalPaid = paymentAmount.multiply(
installment.getPrincipalAmount().divide(installment.getTotalAmount(), 4, BigDecimal.ROUND_HALF_UP));

loanAccount.setOutstandingAmount(loanAccount.getOutstandingAmount().subtract(principalPaid));
loanAccount.setInterestAccrued(loanAccount.getInterestAccrued().subtract(
paymentAmount.subtract(principalPaid)));
}

// 保存贷款账户更新
loanAccountRepository.save(loanAccount);

// 记录交易
transactionService.withdraw(
loanAccount.getAccountNumber(),
request.getAmount(),
"Loan repayment for " + loanAccountNo + ", Ref: " + request.getTransactionReference());

log.info("Repayment received for loan account: {}, amount: {}", loanAccountNo, request.getAmount());

return convertToDTO(dueInstallments.get(0));
}

@Scheduled(cron = "0 0 0 * * ?") // 每天午夜执行
public void checkOverdueLoans() {
LocalDate today = LocalDate.now();
List<LoanAccount> dueLoans = loanAccountRepository.findDueLoans(
List.of(LoanAccountStatus.ACTIVE, LoanAccountStatus.DELINQUENT), today);

dueLoans.forEach(loan -> {
try {
updateOverdueStatus(loan.getLoanAccountNo(), today);
} catch (Exception e) {
log.error("Failed to update overdue status for loan: " + loan.getLoanAccountNo(), e);
}
});
}

@Transactional
public void updateOverdueStatus(String loanAccountNo, LocalDate asOfDate) {
LoanAccount loanAccount = loanAccountRepository.findByLoanAccountNoForUpdate(loanAccountNo)
.orElseThrow(() -> new AccountException(ErrorCode.LOAN_ACCOUNT_NOT_FOUND));

// 查找逾期的还款计划
List<RepaymentSchedule> overdueInstallments = repaymentRepository
.findDueInstallments(
loanAccountNo,
asOfDate,
List.of(RepaymentStatus.PENDING));

if (!overdueInstallments.isEmpty()) {
overdueInstallments.forEach(installment -> {
installment.setStatus(RepaymentStatus.OVERDUE);
repaymentRepository.save(installment);
});

loanAccount.setStatus(LoanAccountStatus.DELINQUENT);
loanAccountRepository.save(loanAccount);

log.info("Loan account marked as delinquent: {}", loanAccountNo);
}
}

private List<RepaymentSchedule> generateRepaymentSchedule(LoanApplication application) {
// 简化处理,生成等额本息还款计划
BigDecimal monthlyRate = application.getInterestRate().divide(BigDecimal.valueOf(12), 6, BigDecimal.ROUND_HALF_UP);
BigDecimal monthlyPayment = calculateMonthlyPayment(application.getAmount(), monthlyRate, application.getTerm());

LocalDate paymentDate = LocalDate.now().plusMonths(1);
BigDecimal remainingPrincipal = application.getAmount();

List<RepaymentSchedule> schedules = new ArrayList<>();

for (int i = 1; i <= application.getTerm(); i++) {
BigDecimal interest = remainingPrincipal.multiply(monthlyRate);
BigDecimal principal = monthlyPayment.subtract(interest);

if (i == application.getTerm()) {
principal = remainingPrincipal;
}

RepaymentSchedule schedule = new RepaymentSchedule();
schedule.setInstallmentNo(i);
schedule.setDueDate(paymentDate);
schedule.setPrincipalAmount(principal);
schedule.setInterestAmount(interest);
schedule.setTotalAmount(principal.add(interest));
schedule.setOutstandingPrincipal(remainingPrincipal);
schedule.setStatus(RepaymentStatus.PENDING);

schedules.add(schedule);

remainingPrincipal = remainingPrincipal.subtract(principal);
paymentDate = paymentDate.plusMonths(1);
}

return schedules;
}

private BigDecimal calculateMonthlyPayment(BigDecimal principal, BigDecimal monthlyRate, int term) {
// 等额本息计算公式
BigDecimal temp = BigDecimal.ONE.add(monthlyRate).pow(term);
return principal.multiply(monthlyRate)
.multiply(temp)
.divide(temp.subtract(BigDecimal.ONE), 2, BigDecimal.ROUND_HALF_UP);
}

private LoanAccountDTO convertToDTO(LoanAccount loanAccount) {
LoanAccountDTO dto = new LoanAccountDTO();
dto.setLoanAccountNo(loanAccount.getLoanAccountNo());
dto.setApplicationNo(loanAccount.getApplicationNo());
dto.setCustomerId(loanAccount.getCustomerId());
dto.setAccountNumber(loanAccount.getAccountNumber());
dto.setOriginalAmount(loanAccount.getOriginalAmount());
dto.setOutstandingAmount(loanAccount.getOutstandingAmount());
dto.setInterestAccrued(loanAccount.getInterestAccrued());
dto.setInterestRate(loanAccount.getInterestRate());
dto.setStartDate(loanAccount.getStartDate());
dto.setMaturityDate(loanAccount.getMaturityDate());
dto.setStatus(loanAccount.getStatus());
return dto;
}

private RepaymentDTO convertToDTO(RepaymentSchedule schedule) {
RepaymentDTO dto = new RepaymentDTO();
dto.setId(schedule.getId());
dto.setLoanAccountNo(schedule.getLoanAccountNo());
dto.setInstallmentNo(schedule.getInstallmentNo());
dto.setDueDate(schedule.getDueDate());
dto.setPrincipalAmount(schedule.getPrincipalAmount());
dto.setInterestAmount(schedule.getInterestAmount());
dto.setTotalAmount(schedule.getTotalAmount());
dto.setOutstandingPrincipal(schedule.getOutstandingPrincipal());
dto.setStatus(schedule.getStatus());
dto.setPaidDate(schedule.getPaidDate());
dto.setPaidAmount(schedule.getPaidAmount());
return dto;
}
}

Repository层

public interface LoanApplicationRepository extends JpaRepository<LoanApplication, Long> {
Optional<LoanApplication> findByApplicationNo(String applicationNo);

List<LoanApplication> findByCustomerId(Long customerId);

List<LoanApplication> findByStatus(LoanStatus status);

@Query("SELECT la FROM LoanApplication la WHERE la.status = :status AND la.disbursementDate <= :date")
List<LoanApplication> findPendingDisbursement(@Param("status") LoanStatus status,
@Param("date") LocalDate date);
}

public interface LoanAccountRepository extends JpaRepository<LoanAccount, Long> {
Optional<LoanAccount> findByLoanAccountNo(String loanAccountNo);

List<LoanAccount> findByCustomerId(Long customerId);

List<LoanAccount> findByStatus(LoanAccountStatus status);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT la FROM LoanAccount la WHERE la.loanAccountNo = :loanAccountNo")
Optional<LoanAccount> findByLoanAccountNoForUpdate(@Param("loanAccountNo") String loanAccountNo);

@Query("SELECT la FROM LoanAccount la WHERE la.status IN :statuses AND la.maturityDate <= :date")
List<LoanAccount> findDueLoans(@Param("statuses") List<LoanAccountStatus> statuses,
@Param("date") LocalDate date);
}

public interface RepaymentScheduleRepository extends JpaRepository<RepaymentSchedule, Long> {
List<RepaymentSchedule> findByLoanAccountNo(String loanAccountNo);

List<RepaymentSchedule> findByLoanAccountNoAndStatus(String loanAccountNo, RepaymentStatus status);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT rs FROM RepaymentSchedule rs WHERE rs.id = :id")
Optional<RepaymentSchedule> findByIdForUpdate(@Param("id") Long id);

@Query("SELECT rs FROM RepaymentSchedule rs WHERE rs.loanAccountNo = :loanAccountNo AND rs.dueDate <= :date AND rs.status IN :statuses")
List<RepaymentSchedule> findDueInstallments(@Param("loanAccountNo") String loanAccountNo,
@Param("date") LocalDate date,
@Param("statuses") List<RepaymentStatus> statuses);
}

public interface LoanProductRepository extends JpaRepository<LoanProduct, Long> {
Optional<LoanProduct> findByProductCode(String productCode);

List<LoanProduct> findByLoanType(LoanType loanType);

List<LoanProduct> findByActiveTrue();
}

5. 客户关系管理(CRM)

客户信息管理

客户分级

客户行为分析

客户服务记录

# 在原有配置基础上添加
crm:
customer:
tier-review-interval: 180 # 客户等级评审间隔天数
encryption:
enabled: true

@Entity
@Table(name = "customer")
@Data
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String customerId;

@Column(nullable = false)
private String firstName;

@Column(nullable = false)
private String lastName;

@Column(nullable = false, unique = true)
private String idNumber;

@Column(nullable = false)
private LocalDate dateOfBirth;

@Column(nullable = false)
private String email;

@Column(nullable = false)
private String phone;

@Column(nullable = false)
private String address;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private CustomerTier tier;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private CustomerStatus status;

@Column(nullable = false, precision = 19, scale = 4)
private BigDecimal creditScore;

@Column
private LocalDate lastReviewDate;

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;

@Version
private Long version;
}

@Entity
@Table(name = "customer_interaction")
@Data
public class CustomerInteraction {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private Long customerId;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private InteractionType type;

@Column(nullable = false)
private String description;

@Column
private Long relatedAccountId;

@Column
private String notes;

@Column(nullable = false)
private Long employeeId;

@CreationTimestamp
private LocalDateTime createdAt;
}

@Entity
@Table(name = "customer_preference")
@Data
public class CustomerPreference {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private Long customerId;

@Column(nullable = false)
private String preferenceType;

@Column(nullable = false)
private String preferenceValue;

@Column
private Boolean isActive = true;
}

@Entity
@Table(name = "customer_segment")
@Data
public class CustomerSegment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String segmentCode;

@Column(nullable = false)
private String segmentName;

@Column
private String description;

@Column(nullable = false)
private BigDecimal minValueScore;

@Column(nullable = false)
private BigDecimal maxValueScore;

@Column(nullable = false)
private Boolean isActive = true;
}

public enum CustomerTier {
BASIC, // 基础客户
SILVER, // 银牌客户
GOLD, // 金牌客户
PLATINUM, // 白金客户
DIAMOND // 钻石客户
}

public enum CustomerStatus {
ACTIVE, // 活跃
INACTIVE, // 不活跃
SUSPENDED, // 暂停
BLACKLISTED, // 黑名单
CLOSED // 已关闭
}

public enum InteractionType {
PHONE_CALL, // 电话
EMAIL, // 邮件
IN_PERSON, // 面谈
CHAT, // 在线聊天
COMPLAINT, // 投诉
COMPLIMENT, // 表扬
SERVICE_REQUEST // 服务请求
}

@Data
public class CustomerDTO {
private Long id;
private String customerId;
private String firstName;
private String lastName;
private String idNumber;
private LocalDate dateOfBirth;
private String email;
private String phone;
private String address;
private CustomerTier tier;
private CustomerStatus status;
private BigDecimal creditScore;
private LocalDate lastReviewDate;
private LocalDateTime createdAt;
}

@Data
public class CustomerCreateRequest {
@NotBlank
private String firstName;

@NotBlank
private String lastName;

@NotBlank
private String idNumber;

@NotNull
private LocalDate dateOfBirth;

@Email
@NotBlank
private String email;

@NotBlank
private String phone;

@NotBlank
private String address;
}

@Data
public class CustomerUpdateRequest {
@NotBlank
private String firstName;

@NotBlank
private String lastName;

@Email
@NotBlank
private String email;

@NotBlank
private String phone;

@NotBlank
private String address;
}

@Data
public class CustomerInteractionDTO {
private Long id;
private Long customerId;
private InteractionType type;
private String description;
private Long relatedAccountId;
private String notes;
private Long employeeId;
private LocalDateTime createdAt;
}

@Data
public class CustomerSegmentDTO {
private Long id;
private String segmentCode;
private String segmentName;
private String description;
private BigDecimal minValueScore;
private BigDecimal maxValueScore;
private Boolean isActive;
}

@Data
public class CustomerAnalysisDTO {
private Long customerId;
private String customerName;
private BigDecimal totalAssets;
private BigDecimal totalLiabilities;
private BigDecimal netWorth;
private BigDecimal monthlyIncome;
private BigDecimal monthlyExpenses;
private BigDecimal profitabilityScore;
private LocalDate lastActivityDate;
private Integer productCount;
}

工具

@Component
public class CustomerIdGenerator {
private static final String BANK_CODE = "888";
private final AtomicLong sequence = new AtomicLong(1);

public String generate() {
String dateStr = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
long seq = sequence.getAndIncrement();
return String.format("%s-CUS-%s-%06d", BANK_CODE, dateStr, seq);
}
}

Controller层

@RestController
@RequestMapping("/api/customers")
@RequiredArgsConstructor
public class CustomerController {
private final CustomerService customerService;

@PostMapping
public CustomerDTO createCustomer(@Valid @RequestBody CustomerCreateRequest request) {
return customerService.createCustomer(request);
}

@PutMapping("/{customerId}")
public CustomerDTO updateCustomer(
@PathVariable String customerId,
@Valid @RequestBody CustomerUpdateRequest request) {
return customerService.updateCustomer(customerId, request);
}

@GetMapping("/{customerId}")
public CustomerDTO getCustomer(@PathVariable String customerId) {
return customerService.getCustomer(customerId);
}

@GetMapping("/tier/{tier}")
public List<CustomerDTO> getCustomersByTier(@PathVariable String tier) {
return customerService.getCustomersByTier(tier);
}

@PutMapping("/{customerId}/tier/{tier}")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
public CustomerDTO updateCustomerTier(
@PathVariable String customerId,
@PathVariable String tier) {
return customerService.updateCustomerTier(customerId, tier);
}

@PutMapping("/{customerId}/status/{status}")
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
public CustomerDTO updateCustomerStatus(
@PathVariable String customerId,
@PathVariable String status) {
return customerService.updateCustomerStatus(customerId, status);
}

@GetMapping("/{customerId}/analysis")
public CustomerAnalysisDTO getCustomerAnalysis(@PathVariable String customerId) {
return customerService.getCustomerAnalysis(customerId);
}
}

@RestController
@RequestMapping("/api/customer-interactions")
@RequiredArgsConstructor
public class CustomerInteractionController {
private final CustomerInteractionService interactionService;

@PostMapping
public CustomerInteractionDTO recordInteraction(
@RequestParam String customerId,
@RequestParam InteractionType type,
@RequestParam String description,
@RequestParam(required = false) Long relatedAccountId,
@RequestParam(required = false) String notes,
@RequestParam Long employeeId) {

return interactionService.recordInteraction(
customerId, type, description, relatedAccountId, notes, employeeId);
}

@GetMapping("/customer/{customerId}")
public List<CustomerInteractionDTO> getCustomerInteractions(@PathVariable String customerId) {
return interactionService.getCustomerInteractions(customerId);
}

@GetMapping("/employee/{employeeId}")
public List<CustomerInteractionDTO> getEmployeeInteractions(
@PathVariable Long employeeId,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime end) {

return interactionService.getEmployeeInteractions(employeeId, start, end);
}
}

Service层

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomerService {
private final CustomerRepository customerRepository;
private final CustomerIdGenerator idGenerator;
private final EncryptionUtil encryptionUtil;
private final CustomerAnalysisService analysisService;

@Transactional
public CustomerDTO createCustomer(CustomerCreateRequest request) {
// 检查身份证号是否已存在
if (customerRepository.findByIdNumber(request.getIdNumber()).isPresent()) {
throw new AccountException(ErrorCode.CUSTOMER_ALREADY_EXISTS);
}

// 创建客户
Customer customer = new Customer();
customer.setCustomerId(idGenerator.generate());
customer.setFirstName(request.getFirstName());
customer.setLastName(request.getLastName());
customer.setIdNumber(encryptionUtil.encrypt(request.getIdNumber()));
customer.setDateOfBirth(request.getDateOfBirth());
customer.setEmail(request.getEmail());
customer.setPhone(encryptionUtil.encrypt(request.getPhone()));
customer.setAddress(request.getAddress());
customer.setTier(CustomerTier.BASIC);
customer.setStatus(CustomerStatus.ACTIVE);
customer.setCreditScore(calculateInitialCreditScore());

Customer saved = customerRepository.save(customer);
log.info("Customer created: {}", saved.getCustomerId());

// 初始化客户分析数据
analysisService.initializeCustomerAnalysis(saved.getId());

return convertToDTO(saved);
}

@Transactional
public CustomerDTO updateCustomer(String customerId, CustomerUpdateRequest request) {
Customer customer = customerRepository.findByCustomerId(customerId)
.orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));

customer.setFirstName(request.getFirstName());
customer.setLastName(request.getLastName());
customer.setEmail(request.getEmail());
customer.setPhone(encryptionUtil.encrypt(request.getPhone()));
customer.setAddress(request.getAddress());

Customer saved = customerRepository.save(customer);
log.info("Customer updated: {}", customerId);

return convertToDTO(saved);
}

@Transactional(readOnly = true)
public CustomerDTO getCustomer(String customerId) {
Customer customer = customerRepository.findByCustomerId(customerId)
.orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));

CustomerDTO dto = convertToDTO(customer);
dto.setPhone(encryptionUtil.decrypt(dto.getPhone()));
return dto;
}

@Transactional(readOnly = true)
public List<CustomerDTO> getCustomersByTier(String tier) {
return customerRepository.findByTier(CustomerTier.valueOf(tier))
.stream()
.map(this::convertToDTO)
.peek(dto -> dto.setPhone(encryptionUtil.decrypt(dto.getPhone())))
.collect(Collectors.toList());
}

@Transactional
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
public CustomerDTO updateCustomerTier(String customerId, String tier) {
Customer customer = customerRepository.findByCustomerId(customerId)
.orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));

customer.setTier(CustomerTier.valueOf(tier));
customer.setLastReviewDate(LocalDate.now());

Customer saved = customerRepository.save(customer);
log.info("Customer tier updated to {}: {}", tier, customerId);

return convertToDTO(saved);
}

@Transactional
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
public CustomerDTO updateCustomerStatus(String customerId, String status) {
Customer customer = customerRepository.findByCustomerId(customerId)
.orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));

customer.setStatus(CustomerStatus.valueOf(status));

Customer saved = customerRepository.save(customer);
log.info("Customer status updated to {}: {}", status, customerId);

return convertToDTO(saved);
}

@Transactional(readOnly = true)
public CustomerAnalysisDTO getCustomerAnalysis(String customerId) {
Customer customer = customerRepository.findByCustomerId(customerId)
.orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));

return analysisService.getCustomerAnalysis(customer.getId());
}

@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行
public void reviewCustomerTiers() {
LocalDate reviewDate = LocalDate.now().minusMonths(6);
List<Customer> customers = customerRepository.findCustomersNeedingReview(reviewDate);

customers.forEach(customer -> {
try {
reviewCustomerTier(customer);
} catch (Exception e) {
log.error("Failed to review customer tier: " + customer.getCustomerId(), e);
}
});
}

private void reviewCustomerTier(Customer customer) {
CustomerAnalysisDTO analysis = analysisService.getCustomerAnalysis(customer.getId());

CustomerTier newTier = determineCustomerTier(analysis);
if (newTier != customer.getTier()) {
customer.setTier(newTier);
customer.setLastReviewDate(LocalDate.now());
customerRepository.save(customer);
log.info("Customer tier updated to {}: {}", newTier, customer.getCustomerId());
}
}

private CustomerTier determineCustomerTier(CustomerAnalysisDTO analysis) {
BigDecimal score = analysis.getProfitabilityScore();

if (score.compareTo(BigDecimal.valueOf(90)) >= 0) {
return CustomerTier.DIAMOND;
} else if (score.compareTo(BigDecimal.valueOf(75)) >= 0) {
return CustomerTier.PLATINUM;
} else if (score.compareTo(BigDecimal.valueOf(60)) >= 0) {
return CustomerTier.GOLD;
} else if (score.compareTo(BigDecimal.valueOf(40)) >= 0) {
return CustomerTier.SILVER;
} else {
return CustomerTier.BASIC;
}
}

private BigDecimal calculateInitialCreditScore() {
// 简化处理,实际业务中可能有更复杂的信用评分计算逻辑
return BigDecimal.valueOf(65); // 初始信用分65
}

private CustomerDTO convertToDTO(Customer customer) {
CustomerDTO dto = new CustomerDTO();
dto.setId(customer.getId());
dto.setCustomerId(customer.getCustomerId());
dto.setFirstName(customer.getFirstName());
dto.setLastName(customer.getLastName());
dto.setIdNumber(customer.getIdNumber());
dto.setDateOfBirth(customer.getDateOfBirth());
dto.setEmail(customer.getEmail());
dto.setPhone(customer.getPhone());
dto.setAddress(customer.getAddress());
dto.setTier(customer.getTier());
dto.setStatus(customer.getStatus());
dto.setCreditScore(customer.getCreditScore());
dto.setLastReviewDate(customer.getLastReviewDate());
dto.setCreatedAt(customer.getCreatedAt());
return dto;
}
}

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomerAnalysisService {
private final CustomerRepository customerRepository;
private final AccountClientService accountClientService;
private final LoanClientService loanClientService;
private final TransactionClientService transactionClientService;

@Transactional
public void initializeCustomerAnalysis(Long customerId) {
// 在实际项目中,这里会初始化客户的分析数据
log.info("Initialized analysis data for customer: {}", customerId);
}

@Transactional(readOnly = true)
public CustomerAnalysisDTO getCustomerAnalysis(Long customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));

CustomerAnalysisDTO analysis = new CustomerAnalysisDTO();
analysis.setCustomerId(customer.getId());
analysis.setCustomerName(customer.getFirstName() + " " + customer.getLastName());

// 获取客户资产数据
BigDecimal totalAssets = accountClientService.getCustomerTotalAssets(customer.getCustomerId());
analysis.setTotalAssets(totalAssets);

// 获取客户负债数据
BigDecimal totalLiabilities = loanClientService.getCustomerTotalLiabilities(customer.getCustomerId());
analysis.setTotalLiabilities(totalLiabilities);

// 计算净资产
analysis.setNetWorth(totalAssets.subtract(totalLiabilities));

// 获取月度收支数据
analysis.setMonthlyIncome(transactionClientService.getMonthlyIncome(customer.getCustomerId()));
analysis.setMonthlyExpenses(transactionClientService.getMonthlyExpenses(customer.getCustomerId()));

// 计算盈利性评分
analysis.setProfitabilityScore(calculateProfitabilityScore(analysis));

// 获取最后活动日期
analysis.setLastActivityDate(transactionClientService.getLastActivityDate(customer.getCustomerId()));

// 获取产品数量
analysis.setProductCount(
accountClientService.getAccountCount(customer.getCustomerId()) +
loanClientService.getLoanCount(customer.getCustomerId()));

return analysis;
}

private BigDecimal calculateProfitabilityScore(CustomerAnalysisDTO analysis) {
// 简化处理,实际业务中可能有更复杂的评分逻辑
BigDecimal score = BigDecimal.ZERO;

// 净资产贡献
if (analysis.getNetWorth().compareTo(BigDecimal.valueOf(1000000)) >= 0) {
score = score.add(BigDecimal.valueOf(40));
} else if (analysis.getNetWorth().compareTo(BigDecimal.valueOf(500000)) >= 0) {
score = score.add(BigDecimal.valueOf(30));
} else if (analysis.getNetWorth().compareTo(BigDecimal.valueOf(100000)) >= 0) {
score = score.add(BigDecimal.valueOf(20));
} else {
score = score.add(BigDecimal.valueOf(10));
}

// 月收入贡献
if (analysis.getMonthlyIncome().compareTo(BigDecimal.valueOf(50000)) >= 0) {
score = score.add(BigDecimal.valueOf(30));
} else if (analysis.getMonthlyIncome().compareTo(BigDecimal.valueOf(20000)) >= 0) {
score = score.add(BigDecimal.valueOf(20));
} else if (analysis.getMonthlyIncome().compareTo(BigDecimal.valueOf(5000)) >= 0) {
score = score.add(BigDecimal.valueOf(15));
} else {
score = score.add(BigDecimal.valueOf(5));
}

// 产品数量贡献
score = score.add(BigDecimal.valueOf(Math.min(analysis.getProductCount(), 10) * 2));

// 活动频率贡献
long daysSinceLastActivity = LocalDate.now().toEpochDay()
analysis.getLastActivityDate().toEpochDay();
if (daysSinceLastActivity <= 7) {
score = score.add(BigDecimal.valueOf(20));
} else if (daysSinceLastActivity <= 30) {
score = score.add(BigDecimal.valueOf(10));
}

return score.setScale(0, RoundingMode.HALF_UP);
}
}

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomerInteractionService {
private final CustomerInteractionRepository interactionRepository;
private final CustomerRepository customerRepository;

@Transactional
public CustomerInteractionDTO recordInteraction(
String customerId,
InteractionType type,
String description,
Long relatedAccountId,
String notes,
Long employeeId) {

Customer customer = customerRepository.findByCustomerId(customerId)
.orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));

CustomerInteraction interaction = new CustomerInteraction();
interaction.setCustomerId(customer.getId());
interaction.setType(type);
interaction.setDescription(description);
interaction.setRelatedAccountId(relatedAccountId);
interaction.setNotes(notes);
interaction.setEmployeeId(employeeId);

CustomerInteraction saved = interactionRepository.save(interaction);
log.info("Interaction recorded for customer: {}", customerId);

return convertToDTO(saved);
}

@Transactional(readOnly = true)
public List<CustomerInteractionDTO> getCustomerInteractions(String customerId) {
Customer customer = customerRepository.findByCustomerId(customerId)
.orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));

return interactionRepository.findByCustomerId(customer.getId())
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}

@Transactional(readOnly = true)
public List<CustomerInteractionDTO> getEmployeeInteractions(
Long employeeId,
LocalDateTime start,
LocalDateTime end) {

return interactionRepository.findByEmployeeIdAndCreatedAtBetween(employeeId, start, end)
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}

private CustomerInteractionDTO convertToDTO(CustomerInteraction interaction) {
CustomerInteractionDTO dto = new CustomerInteractionDTO();
dto.setId(interaction.getId());
dto.setCustomerId(interaction.getCustomerId());
dto.setType(interaction.getType());
dto.setDescription(interaction.getDescription());
dto.setRelatedAccountId(interaction.getRelatedAccountId());
dto.setNotes(interaction.getNotes());
dto.setEmployeeId(interaction.getEmployeeId());
dto.setCreatedAt(interaction.getCreatedAt());
return dto;
}
}

Repository层

public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByCustomerId(String customerId);

Optional<Customer> findByIdNumber(String idNumber);

List<Customer> findByTier(CustomerTier tier);

List<Customer> findByStatus(CustomerStatus status);

List<Customer> findByCreditScoreBetween(BigDecimal minScore, BigDecimal maxScore);

@Query("SELECT c FROM Customer c WHERE c.lastReviewDate IS NULL OR c.lastReviewDate < :date")
List<Customer> findCustomersNeedingReview(@Param("date") LocalDate date);
}

public interface CustomerInteractionRepository extends JpaRepository<CustomerInteraction, Long> {
List<CustomerInteraction> findByCustomerId(Long customerId);

List<CustomerInteraction> findByCustomerIdAndType(Long customerId, InteractionType type);

List<CustomerInteraction> findByEmployeeIdAndCreatedAtBetween(
Long employeeId, LocalDateTime start, LocalDateTime end);
}

public interface CustomerPreferenceRepository extends JpaRepository<CustomerPreference, Long> {
List<CustomerPreference> findByCustomerId(Long customerId);

Optional<CustomerPreference> findByCustomerIdAndPreferenceType(
Long customerId, String preferenceType);

List<CustomerPreference> findByPreferenceTypeAndPreferenceValue(
String preferenceType, String preferenceValue);
}

public interface CustomerSegmentRepository extends JpaRepository<CustomerSegment, Long> {
Optional<CustomerSegment> findBySegmentCode(String segmentCode);

List<CustomerSegment> findByIsActiveTrue();
}

6. 风险管理

反欺诈检测

交易监控

合规检查

风险预警

# 在原有配置基础上添加
risk:
engine:
rules-refresh-interval: 300000 # 规则刷新间隔(5分钟)
blacklist:
cache-enabled: true
cache-ttl: 3600000 # 1小时

@Entity
@Table(name = "risk_rule")
@Data
public class RiskRule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String ruleCode;

@Column(nullable = false)
private String ruleName;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private RiskRuleCategory category;

@Column(nullable = false, columnDefinition = "TEXT")
private String ruleExpression;

@Column(nullable = false)
private Integer priority;

@Column(nullable = false)
private Boolean isActive = true;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private RiskRuleStatus status = RiskRuleStatus.DRAFT;

@Column
private String action;

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;
}

@Entity
@Table(name = "risk_event")
@Data
public class RiskEvent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String eventId;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private RiskEventType eventType;

@Column(nullable = false)
private String ruleCode;

@Column
private Long customerId;

@Column
private String accountNumber;

@Column
private String transactionId;

@Column(precision = 19, scale = 4)
private BigDecimal amount;

@Column(nullable = false)
private String description;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private RiskEventStatus status = RiskEventStatus.PENDING;

@Column
private Long handledBy;

@Column
private LocalDateTime handledAt;

@Column
private String handlingNotes;

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;
}

@Entity
@Table(name = "risk_parameter")
@Data
public class RiskParameter {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String paramKey;

@Column(nullable = false)
private String paramName;

@Column(nullable = false)
private String paramValue;

@Column
private String description;

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;
}

@Entity
@Table(name = "risk_blacklist")
@Data
public class RiskBlacklist {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String entityType; // CUSTOMER, ACCOUNT, IP, DEVICE, etc.

@Column(nullable = false)
private String entityValue;

@Column(nullable = false)
private String reason;

@Column(nullable = false)
private String source;

@CreationTimestamp
private LocalDateTime createdAt;
}

public enum RiskRuleCategory {
FRAUD_DETECTION, // 欺诈检测
AML_COMPLIANCE, // 反洗钱合规
TRANSACTION_MONITORING, // 交易监控
CREDIT_RISK, // 信用风险
OPERATIONAL_RISK // 操作风险
}

public enum RiskRuleStatus {
DRAFT, // 草稿
TESTING, // 测试中
PRODUCTION, // 生产中
DISABLED // 已禁用
}

public enum RiskEventType {
SUSPICIOUS_TRANSACTION, // 可疑交易
HIGH_RISK_CUSTOMER, // 高风险客户
AML_ALERT, // 反洗钱警报
FRAUD_ATTEMPT, // 欺诈尝试
POLICY_VIOLATION // 政策违规
}

public enum RiskEventStatus {
PENDING, // 待处理
REVIEWING, // 审核中
APPROVED, // 已批准
REJECTED, // 已拒绝
MITIGATED // 已缓解
}

@Data
public class RiskRuleDTO {
private Long id;
private String ruleCode;
private String ruleName;
private RiskRuleCategory category;
private String ruleExpression;
private Integer priority;
private Boolean isActive;
private RiskRuleStatus status;
private String action;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

@Data
public class RiskRuleRequest {
@NotBlank
private String ruleName;

@NotNull
private RiskRuleCategory category;

@NotBlank
private String ruleExpression;

@NotNull
@Min(1)
@Max(100)
private Integer priority;

private String action;
}

@Data
public class RiskEventDTO {
private Long id;
private String eventId;
private RiskEventType eventType;
private String ruleCode;
private Long customerId;
private String accountNumber;
private String transactionId;
private BigDecimal amount;
private String description;
private RiskEventStatus status;
private Long handledBy;
private LocalDateTime handledAt;
private String handlingNotes;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

@Data
public class RiskParameterDTO {
private Long id;
private String paramKey;
private String paramName;
private String paramValue;
private String description;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

@Data
public class RiskBlacklistDTO {
private Long id;
private String entityType;
private String entityValue;
private String reason;
private String source;
private LocalDateTime createdAt;
}

@Data
public class RiskAssessmentResult {
private String transactionId;
private BigDecimal riskScore;
private String riskLevel;
private Map<String, String> triggeredRules;
private String recommendation;
}

规则引擎工具

@Component
public class RiskEngineHelper {
public boolean evaluateRule(String ruleExpression, Map<String, Object> data) {
try {
Serializable compiled = MVEL.compileExpression(ruleExpression);
return MVEL.executeExpression(compiled, data, Boolean.class);
} catch (Exception e) {
throw new RuntimeException("Failed to evaluate rule: " + e.getMessage(), e);
}
}
}

Controller层

@RestController
@RequestMapping("/api/risk/rules")
@RequiredArgsConstructor
public class RiskRuleController {
private final RiskRuleService ruleService;

@PostMapping
@PreAuthorize("hasAnyRole('RISK_MANAGER', 'ADMIN')")
public RiskRuleDTO createRule(@Valid @RequestBody RiskRuleRequest request) {
return ruleService.createRule(request);
}

@PutMapping("/{ruleCode}/status/{status}")
@PreAuthorize("hasAnyRole('RISK_MANAGER', 'ADMIN')")
public RiskRuleDTO updateRuleStatus(
@PathVariable String ruleCode,
@PathVariable RiskRuleStatus status) {
return ruleService.updateRuleStatus(ruleCode, status);
}

@PutMapping("/{ruleCode}/active/{isActive}")
@PreAuthorize("hasAnyRole('RISK_MANAGER', 'ADMIN')")
public RiskRuleDTO toggleRuleActivation(
@PathVariable String ruleCode,
@PathVariable Boolean isActive) {
return ruleService.toggleRuleActivation(ruleCode, isActive);
}

@GetMapping("/category/{category}")
public List<RiskRuleDTO> getRulesByCategory(@PathVariable String category) {
return ruleService.getActiveRulesByCategory(RiskRuleCategory.valueOf(category));
}

@GetMapping
public List<RiskRuleDTO> getAllActiveRules() {
return ruleService.getAllActiveRules();
}
}

@RestController
@RequestMapping("/api/risk/assessments")
@RequiredArgsConstructor
public class RiskAssessmentController {
private final RiskEngineService riskEngineService;

@PostMapping("/transactions")
public RiskAssessmentResult assessTransactionRisk(
@RequestParam String transactionId,
@RequestParam String accountNumber,
@RequestParam Long customerId,
@RequestParam BigDecimal amount,
@RequestBody Map<String, Object> transactionData) {

return riskEngineService.assessTransactionRisk(
transactionId, accountNumber, customerId, amount, transactionData);
}

@PostMapping("/customers/{customerId}")
public RiskAssessmentResult assessCustomerRisk(
@PathVariable Long customerId,
@RequestBody Map<String, Object> customerData) {

return riskEngineService.assessCustomerRisk(customerId, customerData);
}
}

@RestController
@RequestMapping("/api/risk/events")
@RequiredArgsConstructor
public class RiskEventController {
private final RiskEventService eventService;

@GetMapping("/pending")
@PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
public List<RiskEventDTO> getPendingEvents() {
return eventService.getPendingEvents();
}

@GetMapping("/customers/{customerId}")
@PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
public List<RiskEventDTO> getCustomerEvents(
@PathVariable Long customerId,
@RequestParam(defaultValue = "30") int days) {
return eventService.getEventsByCustomer(customerId, days);
}

@PutMapping("/{eventId}/status/{status}")
@PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
public RiskEventDTO updateEventStatus(
@PathVariable String eventId,
@PathVariable RiskEventStatus status,
@RequestParam String notes,
@RequestParam Long userId) {
return eventService.updateEventStatus(eventId, status, notes, userId);
}
}

@RestController
@RequestMapping("/api/risk/blacklist")
@RequiredArgsConstructor
public class RiskBlacklistController {
private final RiskBlacklistService blacklistService;

@PostMapping
@PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
public RiskBlacklistDTO addToBlacklist(
@RequestParam String entityType,
@RequestParam String entityValue,
@RequestParam String reason,
@RequestParam String source) {
return blacklistService.addToBlacklist(entityType, entityValue, reason, source);
}

@DeleteMapping("/{id}")
@PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
public void removeFromBlacklist(@PathVariable Long id) {
blacklistService.removeFromBlacklist(id);
}

@GetMapping("/check")
public boolean isBlacklisted(
@RequestParam String entityType,
@RequestParam String entityValue) {
return blacklistService.isBlacklisted(entityType, entityValue);
}

@GetMapping("/type/{entityType}")
@PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
public List<RiskBlacklistDTO> getBlacklistByType(@PathVariable String entityType) {
return blacklistService.getBlacklistByType(entityType);
}
}

Service层

@Service
@RequiredArgsConstructor
@Slf4j
public class RiskRuleService {
private final RiskRuleRepository ruleRepository;
private final RuleCodeGenerator codeGenerator;

@Transactional
@PreAuthorize("hasAnyRole('RISK_MANAGER', 'ADMIN')")
public RiskRuleDTO createRule(RiskRuleRequest request) {
RiskRule rule = new RiskRule();
rule.setRuleCode(codeGenerator.generate());
rule.setRuleName(request.getRuleName());
rule.setCategory(request.getCategory());
rule.setRuleExpression(request.getRuleExpression());
rule.setPriority(request.getPriority());
rule.setAction(request.getAction());

RiskRule saved = ruleRepository.save(rule);
log.info("Risk rule created: {}", saved.getRuleCode());

return convertToDTO(saved);
}

@Transactional
@PreAuthorize("hasAnyRole('RISK_MANAGER', 'ADMIN')")
public RiskRuleDTO updateRuleStatus(String ruleCode, RiskRuleStatus status) {
RiskRule rule = ruleRepository.findByRuleCode(ruleCode)
.orElseThrow(() -> new AccountException(ErrorCode.RISK_RULE_NOT_FOUND));

rule.setStatus(status);
RiskRule saved = ruleRepository.save(rule);
log.info("Risk rule status updated to {}: {}", status, ruleCode);

return convertToDTO(saved);
}

@Transactional
@PreAuthorize("hasAnyRole('RISK_MANAGER', 'ADMIN')")
public RiskRuleDTO toggleRuleActivation(String ruleCode, Boolean isActive) {
RiskRule rule = ruleRepository.findByRuleCode(ruleCode)
.orElseThrow(() -> new AccountException(ErrorCode.RISK_RULE_NOT_FOUND));

rule.setIsActive(isActive);
RiskRule saved = ruleRepository.save(rule);
log.info("Risk rule activation set to {}: {}", isActive, ruleCode);

return convertToDTO(saved);
}

@Transactional(readOnly = true)
public List<RiskRuleDTO> getActiveRulesByCategory(RiskRuleCategory category) {
return ruleRepository.findByCategoryAndIsActiveTrue(category)
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}

@Transactional(readOnly = true)
public List<RiskRuleDTO> getAllActiveRules() {
return ruleRepository.findAllActiveRules()
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}

private RiskRuleDTO convertToDTO(RiskRule rule) {
RiskRuleDTO dto = new RiskRuleDTO();
dto.setId(rule.getId());
dto.setRuleCode(rule.getRuleCode());
dto.setRuleName(rule.getRuleName());
dto.setCategory(rule.getCategory());
dto.setRuleExpression(rule.getRuleExpression());
dto.setPriority(rule.getPriority());
dto.setIsActive(rule.getIsActive());
dto.setStatus(rule.getStatus());
dto.setAction(rule.getAction());
dto.setCreatedAt(rule.getCreatedAt());
dto.setUpdatedAt(rule.getUpdatedAt());
return dto;
}
}

/**
风险管理引擎
*/

@Service
@RequiredArgsConstructor
@Slf4j
public class RiskEngineService {
private final RiskRuleRepository ruleRepository;
private final RiskEventService eventService;
private final RiskEngineHelper engineHelper;

@Transactional
public RiskAssessmentResult assessTransactionRisk(
String transactionId,
String accountNumber,
Long customerId,
BigDecimal amount,
Map<String, Object> transactionData) {

List<RiskRule> activeRules = ruleRepository.findAllActiveRules();
BigDecimal riskScore = BigDecimal.ZERO;
Map<String, String> triggeredRules = new HashMap<>();

for (RiskRule rule : activeRules) {
try {
boolean isTriggered = engineHelper.evaluateRule(rule.getRuleExpression(), transactionData);
if (isTriggered) {
riskScore = riskScore.add(BigDecimal.valueOf(rule.getPriority()));
triggeredRules.put(rule.getRuleCode(), rule.getRuleName());

// 记录风险事件
RiskEventDTO event = new RiskEventDTO();
event.setEventType(RiskEventType.SUSPICIOUS_TRANSACTION);
event.setRuleCode(rule.getRuleCode());
event.setCustomerId(customerId);
event.setAccountNumber(accountNumber);
event.setTransactionId(transactionId);
event.setAmount(amount);
event.setDescription(rule.getRuleName() + " triggered");
event.setStatus(RiskEventStatus.PENDING);

eventService.createEvent(event);
}
} catch (Exception e) {
log.error("Error evaluating risk rule {}: {}", rule.getRuleCode(), e.getMessage());
}
}

RiskAssessmentResult result = new RiskAssessmentResult();
result.setTransactionId(transactionId);
result.setRiskScore(riskScore);
result.setRiskLevel(determineRiskLevel(riskScore));
result.setTriggeredRules(triggeredRules);
result.setRecommendation(generateRecommendation(riskScore));

log.info("Risk assessment completed for transaction {}: score {}", transactionId, riskScore);

return result;
}

@Transactional
public RiskAssessmentResult assessCustomerRisk(Long customerId, Map<String, Object> customerData) {
List<RiskRule> activeRules = ruleRepository.findAllActiveRules();
BigDecimal riskScore = BigDecimal.ZERO;
Map<String, String> triggeredRules = new HashMap<>();

for (RiskRule rule : activeRules) {
try {
boolean isTriggered = engineHelper.evaluateRule(rule.getRuleExpression(), customerData);
if (isTriggered) {
riskScore = riskScore.add(BigDecimal.valueOf(rule.getPriority()));
triggeredRules.put(rule.getRuleCode(), rule.getRuleName());

// 记录风险事件
RiskEventDTO event = new RiskEventDTO();
event.setEventType(RiskEventType.HIGH_RISK_CUSTOMER);
event.setRuleCode(rule.getRuleCode());
event.setCustomerId(customerId);
event.setDescription(rule.getRuleName() + " triggered");
event.setStatus(RiskEventStatus.PENDING);

eventService.createEvent(event);
}
} catch (Exception e) {
log.error("Error evaluating risk rule {}: {}", rule.getRuleCode(), e.getMessage());
}
}

RiskAssessmentResult result = new RiskAssessmentResult();
result.setRiskScore(riskScore);
result.setRiskLevel(determineRiskLevel(riskScore));
result.setTriggeredRules(triggeredRules);
result.setRecommendation(generateRecommendation(riskScore));

log.info("Risk assessment completed for customer {}: score {}", customerId, riskScore);

return result;
}

private String determineRiskLevel(BigDecimal score) {
if (score.compareTo(BigDecimal.valueOf(80)) >= 0) {
return "CRITICAL";
} else if (score.compareTo(BigDecimal.valueOf(50)) >= 0) {
return "HIGH";
} else if (score.compareTo(BigDecimal.valueOf(30)) >= 0) {
return "MEDIUM";
} else if (score.compareTo(BigDecimal.valueOf(10)) >= 0) {
return "LOW";
} else {
return "NORMAL";
}
}

private String generateRecommendation(BigDecimal score) {
if (score.compareTo(BigDecimal.valueOf(80)) >= 0) {
return "BLOCK and ALERT";
} else if (score.compareTo(BigDecimal.valueOf(50)) >= 0) {
return "REVIEW and VERIFY";
} else if (score.compareTo(BigDecimal.valueOf(30)) >= 0) {
return "MONITOR CLOSELY";
} else if (score.compareTo(BigDecimal.valueOf(10)) >= 0) {
return "STANDARD MONITORING";
} else {
return "NO ACTION REQUIRED";
}
}
}

/**
风险事件服务
*/

@Service
@RequiredArgsConstructor
@Slf4j
public class RiskEventService {
private final RiskEventRepository eventRepository;
private final EventIdGenerator idGenerator;

@Transactional
public RiskEventDTO createEvent(RiskEventDTO eventDTO) {
RiskEvent event = new RiskEvent();
event.setEventId(idGenerator.generate());
event.setEventType(eventDTO.getEventType());
event.setRuleCode(eventDTO.getRuleCode());
event.setCustomerId(eventDTO.getCustomerId());
event.setAccountNumber(eventDTO.getAccountNumber());
event.setTransactionId(eventDTO.getTransactionId());
event.setAmount(eventDTO.getAmount());
event.setDescription(eventDTO.getDescription());
event.setStatus(eventDTO.getStatus());

RiskEvent saved = eventRepository.save(event);
log.info("Risk event created: {}", saved.getEventId());

return convertToDTO(saved);
}

@Transactional
@PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
public RiskEventDTO updateEventStatus(String eventId, RiskEventStatus status, String notes, Long userId) {
RiskEvent event = eventRepository.findByEventId(eventId)
.orElseThrow(() -> new AccountException(ErrorCode.RISK_EVENT_NOT_FOUND));

event.setStatus(status);
event.setHandledBy(userId);
event.setHandledAt(LocalDateTime.now());
event.setHandlingNotes(notes);

RiskEvent saved = eventRepository.save(event);
log.info("Risk event status updated to {}: {}", status, eventId);

return convertToDTO(saved);
}

@Transactional(readOnly = true)
public List<RiskEventDTO> getPendingEvents() {
return eventRepository.findPendingEvents()
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}

@Transactional(readOnly = true)
public List<RiskEventDTO> getEventsByCustomer(Long customerId, int days) {
LocalDateTime date = LocalDateTime.now().minusDays(days);
return eventRepository.findByCustomerIdAndCreatedAtAfter(customerId, date)
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}

private RiskEventDTO convertToDTO(RiskEvent event) {
RiskEventDTO dto = new RiskEventDTO();
dto.setId(event.getId());
dto.setEventId(event.getEventId());
dto.setEventType(event.getEventType());
dto.setRuleCode(event.getRuleCode());
dto.setCustomerId(event.getCustomerId());
dto.setAccountNumber(event.getAccountNumber());
dto.setTransactionId(event.getTransactionId());
dto.setAmount(event.getAmount());
dto.setDescription(event.getDescription());
dto.setStatus(event.getStatus());
dto.setHandledBy(event.getHandledBy());
dto.setHandledAt(event.getHandledAt());
dto.setHandlingNotes(event.getHandlingNotes());
dto.setCreatedAt(event.getCreatedAt());
dto.setUpdatedAt(event.getUpdatedAt());
return dto;
}
}

/**
黑名单服务
*/

@Service
@RequiredArgsConstructor
@Slf4j
public class RiskBlacklistService {
private final RiskBlacklistRepository blacklistRepository;

@Transactional
@PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
public RiskBlacklistDTO addToBlacklist(String entityType, String entityValue, String reason, String source) {
if (blacklistRepository.findByEntityTypeAndEntityValue(entityType, entityValue).isPresent()) {
throw new AccountException(ErrorCode.ENTITY_ALREADY_BLACKLISTED);
}

RiskBlacklist entry = new RiskBlacklist();
entry.setEntityType(entityType);
entry.setEntityValue(entityValue);
entry.setReason(reason);
entry.setSource(source);

RiskBlacklist saved = blacklistRepository.save(entry);
log.info("Added to blacklist: {} – {}", entityType, entityValue);

return convertToDTO(saved);
}

@Transactional
@PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
public void removeFromBlacklist(Long id) {
RiskBlacklist entry = blacklistRepository.findById(id)
.orElseThrow(() -> new AccountException(ErrorCode.BLACKLIST_ENTRY_NOT_FOUND));

blacklistRepository.delete(entry);
log.info("Removed from blacklist: {} – {}", entry.getEntityType(), entry.getEntityValue());
}

@Transactional(readOnly = true)
public boolean isBlacklisted(String entityType, String entityValue) {
return blacklistRepository.findByEntityTypeAndEntityValue(entityType, entityValue).isPresent();
}

@Transactional(readOnly = true)
public List<RiskBlacklistDTO> getBlacklistByType(String entityType) {
return blacklistRepository.findByEntityType(entityType)
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}

private RiskBlacklistDTO convertToDTO(RiskBlacklist entry) {
RiskBlacklistDTO dto = new RiskBlacklistDTO();
dto.setId(entry.getId());
dto.setEntityType(entry.getEntityType());
dto.setEntityValue(entry.getEntityValue());
dto.setReason(entry.getReason());
dto.setSource(entry.getSource());
dto.setCreatedAt(entry.getCreatedAt());
return dto;
}
}

Repository层

public interface RiskRuleRepository extends JpaRepository<RiskRule, Long> {
Optional<RiskRule> findByRuleCode(String ruleCode);

List<RiskRule> findByCategoryAndIsActiveTrue(RiskRuleCategory category);

List<RiskRule> findByStatus(RiskRuleStatus status);

@Query("SELECT r FROM RiskRule r WHERE r.isActive = true AND r.status = 'PRODUCTION' ORDER BY r.priority DESC")
List<RiskRule> findAllActiveRules();
}

public interface RiskEventRepository extends JpaRepository<RiskEvent, Long> {
Optional<RiskEvent> findByEventId(String eventId);

List<RiskEvent> findByEventTypeAndStatus(RiskEventType eventType, RiskEventStatus status);

List<RiskEvent> findByCustomerIdAndCreatedAtAfter(Long customerId, LocalDateTime date);

@Query("SELECT e FROM RiskEvent e WHERE e.status = 'PENDING' ORDER BY e.createdAt DESC")
List<RiskEvent> findPendingEvents();

@Query("SELECT e FROM RiskEvent e WHERE e.createdAt BETWEEN :start AND :end")
List<RiskEvent> findEventsBetweenDates(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
}

public interface RiskParameterRepository extends JpaRepository<RiskParameter, Long> {
Optional<RiskParameter> findByParamKey(String paramKey);
}

public interface RiskBlacklistRepository extends JpaRepository<RiskBlacklist, Long> {
Optional<RiskBlacklist> findByEntityTypeAndEntityValue(String entityType, String entityValue);

List<RiskBlacklist> findByEntityType(String entityType);
}

赞(0)
未经允许不得转载:网硕互联帮助中心 » 企业级Java项目金融应用领域——银行系统
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!