当前位置: 首页 > news >正文

企业级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=UTCusername: rootpassword: passworddriver-class-name: com.mysql.cj.jdbc.Driverjpa:hibernate:ddl-auto: updateshow-sql: trueproperties:hibernate:format_sql: truedialect: org.hibernate.dialect.MySQL8Dialectserver:port: 8080logging:level:com.bank.account: DEBUG
security:jwt:secret-key: your-256-bit-secret-key-change-this-to-something-secureexpiration: 86400000 # 24 hours in millisecondsrefresh-token.expiration: 604800000 # 7 days in millisecondsencryption:key: your-encryption-key-32bytesiv: your-initialization-vector-16bytesrate-limit:enabled: truecapacity: 100refill-rate: 100refill-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";@CreationTimestampprivate Date createdAt;@UpdateTimestampprivate Date updatedAt;@Versionprivate 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;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return List.of(new SimpleGrantedAuthority(role.name()));}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic 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 {@NotNullprivate Long customerId;@NotNullprivate 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;@Beanpublic 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();}@BeanCorsConfigurationSource 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 {@Beanpublic CacheManager cacheManager() {CacheManager cacheManager = Caching.getCachingProvider().getCacheManager();MutableConfiguration<String, byte[]> config = new MutableConfiguration<>();cacheManager.createCache("rate-limit-buckets", config);return cacheManager;}@BeanProxyManager<String> proxyManager(CacheManager cacheManager) {return new JCacheProxyManager<>(cacheManager.getCache("rate-limit-buckets"));}@Beanpublic BucketConfiguration bucketConfiguration() {return BucketConfiguration.builder().addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)))).build();}@Beanpublic 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 {@Overrideprotected 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);}@Overridepublic String getParameter(String name) {String parameter = super.getParameter(name);return parameter != null ? StringEscapeUtils.escapeHtml4(parameter) : null;}@Overridepublic 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;});}@Overridepublic 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;}@Overridepublic Enumeration<String> getParameterNames() {return Collections.enumeration(getParameterMap().keySet());}}
}

Controller层

@RestController
@RequestMapping("/api/accounts")
@RequiredArgsConstructor
public class AccountController {private final AccountService accountService;@PostMappingpublic 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;@Transactionalpublic 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");}@Transactionalpublic 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");}@Transactionalpublic 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");}@Transactionalpublic 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");}@Transactionalpublic 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://account-service:8080security:transaction:max-retry-attempts: 3retry-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;@Columnprivate 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";@Columnprivate String description;@Column(nullable = false)private String reference;@CreationTimestampprivate LocalDateTime createdAt;@UpdateTimestampprivate LocalDateTime updatedAt;@Versionprivate 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 {@NotNullprivate String accountNumber;@NotNull@Positiveprivate BigDecimal amount;private String description;
}
@Data
public class WithdrawalRequest {@NotNullprivate String accountNumber;@NotNull@Positiveprivate BigDecimal amount;private String description;
}
@Data
public class TransferRequest {@NotNullprivate String fromAccountNumber;@NotNullprivate String toAccountNumber;@NotNull@Positiveprivate BigDecimal amount;private String description;
}@Data
public class BatchTransactionRequest {@NotEmpty@Validprivate 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: truetime: "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";@Columnprivate String description;@Columnprivate String callbackUrl;@Columnprivate String notifyUrl;@Columnprivate String thirdPartyTransactionNo;@Columnprivate LocalDateTime paymentTime;@Columnprivate LocalDateTime settlementTime;@CreationTimestampprivate LocalDateTime createdAt;@UpdateTimestampprivate LocalDateTime updatedAt;@Versionprivate 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;@Columnprivate String bankTransactionNo;@Columnprivate LocalDateTime completedTime;@CreationTimestampprivate LocalDateTime createdAt;@UpdateTimestampprivate 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 {@NotBlankprivate String accountNumber;@NotBlankprivate String merchantCode;@NotBlankprivate String merchantOrderNo;@NotNullprivate PaymentOrderType orderType;@NotNull@Positiveprivate BigDecimal amount;@NotBlankprivate 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 {@NotBlankprivate 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;@PostMappingpublic PaymentResponseDTO createPayment(@Valid @RequestBody PaymentRequestDTO request) {return paymentService.createPayment(request);}@GetMapping("/{orderNo}")public PaymentResponseDTO queryPayment(@PathVariable String orderNo) {return paymentService.queryPayment(orderNo);}@GetMappingpublic 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;@Transactionalpublic 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);}@Asyncpublic 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或notifyUrllog.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;@Transactionalpublic 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);}@Asyncpublic 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;@Columnprivate Long approvedBy;@Columnprivate LocalDateTime approvedAt;@Columnprivate String rejectionReason;@Columnprivate LocalDate disbursementDate;@Columnprivate LocalDate maturityDate;@CreationTimestampprivate LocalDateTime createdAt;@UpdateTimestampprivate LocalDateTime updatedAt;@Versionprivate 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;@CreationTimestampprivate LocalDateTime createdAt;@UpdateTimestampprivate LocalDateTime updatedAt;@Versionprivate 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;@Columnprivate LocalDate paidDate;@Column(precision = 19, scale = 4)private BigDecimal paidAmount;@CreationTimestampprivate LocalDateTime createdAt;@UpdateTimestampprivate 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 {@NotNullprivate Long customerId;@NotBlankprivate String accountNumber;@NotNullprivate 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 {@NotNullprivate 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;@NotBlankprivate 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;@PostMappingpublic 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;@Transactionalpublic 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);}});}@Transactionalpublic 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() {// 从安全上下文中获取当前用户IDreturn 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;@Transactionalpublic 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);}@Transactionalpublic 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);}});}@Transactionalpublic 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;@Columnprivate LocalDate lastReviewDate;@CreationTimestampprivate LocalDateTime createdAt;@UpdateTimestampprivate LocalDateTime updatedAt;@Versionprivate 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;@Columnprivate Long relatedAccountId;@Columnprivate String notes;@Column(nullable = false)private Long employeeId;@CreationTimestampprivate 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;@Columnprivate 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;@Columnprivate 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 {@NotBlankprivate String firstName;@NotBlankprivate String lastName;@NotBlankprivate String idNumber;@NotNullprivate LocalDate dateOfBirth;@Email@NotBlankprivate String email;@NotBlankprivate String phone;@NotBlankprivate String address;
}
@Data
public class CustomerUpdateRequest {@NotBlankprivate String firstName;@NotBlankprivate String lastName;@Email@NotBlankprivate String email;@NotBlankprivate String phone;@NotBlankprivate 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;@PostMappingpublic 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;@PostMappingpublic 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;@Transactionalpublic 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);}@Transactionalpublic 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;@Transactionalpublic 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;@Transactionalpublic 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: truecache-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;@Columnprivate String action;@CreationTimestampprivate LocalDateTime createdAt;@UpdateTimestampprivate 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;@Columnprivate Long customerId;@Columnprivate String accountNumber;@Columnprivate 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;@Columnprivate Long handledBy;@Columnprivate LocalDateTime handledAt;@Columnprivate String handlingNotes;@CreationTimestampprivate LocalDateTime createdAt;@UpdateTimestampprivate 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;@Columnprivate String description;@CreationTimestampprivate LocalDateTime createdAt;@UpdateTimestampprivate 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;@CreationTimestampprivate 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 {@NotBlankprivate String ruleName;@NotNullprivate RiskRuleCategory category;@NotBlankprivate 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));}@GetMappingpublic 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;@Transactionalpublic 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;}@Transactionalpublic 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;@Transactionalpublic 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);
}
http://www.dtcms.com/a/334310.html

相关文章:

  • C#WPF实战出真汁09--【消费开单】--选择菜品
  • 驱动开发系列63 - 配置 nvidia 的 open-gpu-kernel-modules 调试环境
  • AI重构文化基因:从“工具革命”到“生态觉醒”的裂变之路
  • 【101页PPT】芯片半导体企业数字化项目方案汇报(附下载方式)
  • 在鸿蒙应用中快速接入地图功能:从配置到实战案例全解析
  • Nginx域名和IP兼容双方的API地址
  • GaussDB 数据库架构师修炼(十三)安全管理(3)-数据库审计
  • 使用npm/pnpm自身安装指定版本的pnpm
  • JavaWeb开发_Day14
  • 如何在 Ubuntu 24.04 Server 或 Desktop 上安装 XFCE
  • 我的世界Java版1.21.4的Fabric模组开发教程(十八)自定义传送门
  • 边缘计算及其特点
  • 学习日志35 python
  • Python Day30 CSS 定位与弹性盒子详解
  • CodeBuddy IDE深度体验:AI驱动的全栈开发新时代
  • 缓存一致性总线协议(Cache Coherence Protocols)的发展过程
  • LangChain4j:基于 SSE 与 Flux 的 AI 流式对话实现方案
  • Honor of Kings 100star (S40) 2025.08.16
  • 11-verilog的RTC驱动代码
  • 10-verilog的EEPROM驱动-单字节读写
  • OpenCV安装及配置
  • 机器学习核心概念精要:从定义到评估
  • 从频繁告警到平稳发布:服务冷启动 CPU 风暴优化实践222
  • 利用 Java 爬虫按图搜索淘宝商品(拍立淘)实战指南
  • AirReceiverLite:轻松实现手机隔空投屏
  • [typescript] interface和type有什么关系?
  • Spark 数据分发性能深度剖析:mapPartitions vs. UDF – 你该选择哪一个?
  • 矩阵链相乘的最少乘法次数(动态规划解法)
  • KVM虚拟化技术解析:从企业应用到个人创新的开源力量
  • Langfuse2.60.3:独立数据库+docker部署及环境变量详细说明