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

发布于:2025-08-17 ⋅ 阅读:(20) ⋅ 点赞:(0)

银行系统

后端

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

  • 持久层: MyBatis/JPA, Hibernate

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

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

  • API接口: RESTful API, Swagger文档

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

  • 分布式事务: Seata

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

  • 批处理: Spring Batch

前端

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

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

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

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

  • 容器化: Docker + Kubernetes

  • 服务发现: Nacos/Eureka

  • 配置中心: Apollo/Nacos

  • 网关: Spring Cloud Gateway

  • 监控: Prometheus + Grafana

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

  • CI/CD: Jenkins/

  • GitLab CI

1. 账户管理

账户开户/销户

账户信息维护

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

账户余额查询

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

<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
    <!-- Database -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency>
    
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    <!-- JWT支持 -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    
    <!-- 数据加密 -->
    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcpkix-jdk15on</artifactId>
        <version>1.70</version>
    </dependency>
    
    <!-- 防XSS -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-text</artifactId>
        <version>1.10.0</version>
    </dependency>
    
    <!-- 限流 -->
    <dependency>
        <groupId>com.github.vladimir-bukhtoyarov</groupId>
        <artifactId>bucket4j-core</artifactId>
        <version>7.6.0</version>
    </dependency>
    <!-- Other -->
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-ui</artifactId>
        <version>1.6.14</version>
    </dependency>
</dependencies>
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/bank_account_db?useSSL=false&serverTimezone=UTC
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect

server:
  port: 8080

logging:
  level:
    com.bank.account: DEBUG
security:
  jwt:
    secret-key: your-256-bit-secret-key-change-this-to-something-secure
    expiration: 86400000 # 24 hours in milliseconds
    refresh-token.expiration: 604800000 # 7 days in milliseconds
  encryption:
    key: your-encryption-key-32bytes
    iv: your-initialization-vector-16bytes
  rate-limit:
    enabled: true
    capacity: 100
    refill-rate: 100
    refill-time: 1 # minutes
@Entity
@Table(name="bank_account")
@Data
public class Account{
	
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	private Long id;

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

	@Column(nullable = false)
    private Long customerId;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private AccountType accountType;//枚举类型:个人储蓄/活期,企业活期/贷款,信用卡
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private AccountStatus status;//枚举状态:活跃、不活跃、冻结、已关闭、休眠
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal balance = BigDecimal.ZERO;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal availableBalance = BigDecimal.ZERO;
    
    @Column(nullable = false)
    private String currency = "CNY";
    
    @CreationTimestamp
    private Date createdAt;
    
    @UpdateTimestamp
    private Date updatedAt;
    
    @Version
    private Long version; // 乐观锁版本号
}
/**
	安全相关的实体
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String username;
    
    @Column(nullable = false)
    private String password;
    
    @Enumerated(EnumType.STRING)
    private Role role;
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority(role.name()));
    }
    
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    
    @Override
    public boolean isEnabled() {
        return true;
    }
}

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


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


@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationResponse {
    private String token;
    private String refreshToken;
}
public enum AccountType {
    PERSONAL_SAVINGS,    // 个人储蓄账户
    PERSONAL_CURRENT,    // 个人活期账户
    CORPORATE_CURRENT,   // 企业活期账户
    CORPORATE_LOAN,      // 企业贷款账户
    CREDIT_CARD          // 信用卡账户
}

public enum AccountStatus {
    ACTIVE,         // 活跃
    INACTIVE,       // 不活跃
    FROZEN,         // 冻结
    CLOSED,         // 已关闭
    DORMANT         // 休眠
}
@Data
public class AccountDTO {
    private Long id;
    private String accountNumber;
    private Long customerId;
    private AccountType accountType;
    private AccountStatus status;
    private BigDecimal balance;
    private BigDecimal availableBalance;
    private String currency;
    private Date createdAt;
    private Date updatedAt;
}
@Data
public class CreateAccountRequest {
    @NotNull
    private Long customerId;
    
    @NotNull
    private AccountType accountType;
    
    private String currency = "CNY";
}
@Data
public class AccountOperationResponse {
    private boolean success;
    private String message;
    private String accountNumber;
    private BigDecimal newBalance;
}
@Getter
public class AccountException extends RuntimeException {
    private final ErrorCode errorCode;
    
    public AccountException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }
}


@Getter
public enum ErrorCode {
    ACCOUNT_NOT_FOUND(404, "Account not found"),
    ACCOUNT_NOT_ACTIVE(400, "Account is not active"),
    ACCOUNT_ALREADY_FROZEN(400, "Account is already frozen"),
    ACCOUNT_NOT_FROZEN(400, "Account is not frozen"),
    ACCOUNT_ALREADY_CLOSED(400, "Account is already closed"),
    ACCOUNT_BALANCE_NOT_ZERO(400, "Account balance is not zero"),
    INSUFFICIENT_BALANCE(400, "Insufficient balance"),
    INVALID_AMOUNT(400, "Amount must be positive");
    
    private final int status;
    private final String message;
    
    ErrorCode(int status, String message) {
        this.status = status;
        this.message = message;
    }
}

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

安全配置类和限流配置类

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtAuthenticationFilter jwtAuthFilter;
    private final AuthenticationProvider authenticationProvider;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(
                    "/api/auth/**",
                    "/v3/api-docs/**",
                    "/swagger-ui/**",
                    "/swagger-ui.html"
                ).permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("https://bank.com", "https://admin.bank.com"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setExposedHeaders(List.of("X-Rate-Limit-Remaining"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}
@Configuration
@EnableCaching
public class RateLimitConfig {
    @Bean
    public CacheManager cacheManager() {
        CacheManager cacheManager = Caching.getCachingProvider().getCacheManager();
        MutableConfiguration<String, byte[]> config = new MutableConfiguration<>();
        cacheManager.createCache("rate-limit-buckets", config);
        return cacheManager;
    }

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

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

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

JWT认证实现

@Service
public class JwtService {
    @Value("${security.jwt.secret-key}")
    private String secretKey;
    
    @Value("${security.jwt.expiration}")
    private long jwtExpiration;
    
    @Value("${security.jwt.refresh-token.expiration}")
    private long refreshExpiration;

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

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

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

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

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

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

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

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

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

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

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

工具类

/**
	账号生成工具
*/
@Component
public class AccountNumberGenerator {
    private static final String BANK_CODE = "888";
    private final AtomicLong sequence = new AtomicLong(1);
    
    public String generate(AccountType accountType) {
        long seq = sequence.getAndIncrement();
        String prefix = getAccountPrefix(accountType);
        String seqStr = String.format("%010d", seq);
        
        // 简单校验码计算
        String rawNumber = BANK_CODE + prefix + seqStr;
        int checkDigit = calculateCheckDigit(rawNumber);
        
        return rawNumber + checkDigit;
    }
    
    private String getAccountPrefix(AccountType accountType) {
        return switch (accountType) {
            case PERSONAL_SAVINGS -> "10";
            case PERSONAL_CURRENT -> "11";
            case CORPORATE_CURRENT -> "20";
            case CORPORATE_LOAN -> "21";
            case CREDIT_CARD -> "30";
        };
    }
    
    private int calculateCheckDigit(String number) {
        int sum = 0;
        for (int i = 0; i < number.length(); i++) {
            int digit = Character.getNumericValue(number.charAt(i));
            sum += (i % 2 == 0) ? digit * 1 : digit * 3;
        }
        return (10 - (sum % 10)) % 10;
    }
}
/**
	数据加密工具
*/
@Component
public class EncryptionUtil {
    @Value("${security.encryption.key}")
    private String encryptionKey;
    
    @Value("${security.encryption.iv}")
    private String iv;
    
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    public String encrypt(String data) {
        try {
            IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
            SecretKeySpec keySpec = new SecretKeySpec(encryptionKey.getBytes(StandardCharsets.UTF_8), "AES");
            
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
            
            byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("Encryption failed", e);
        }
    }

    public String decrypt(String encryptedData) {
        try {
            IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
            SecretKeySpec keySpec = new SecretKeySpec(encryptionKey.getBytes(StandardCharsets.UTF_8), "AES");
            
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
            
            byte[] decoded = Base64.getDecoder().decode(encryptedData);
            byte[] decrypted = cipher.doFinal(decoded);
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("Decryption failed", e);
        }
    }
}

防XSS过滤器

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

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

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

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

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

        @Override
        public Map<String, String[]> getParameterMap() {
            Map<String, String[]> parameterMap = super.getParameterMap();
            Map<String, String[]> escapedParameterMap = new ConcurrentHashMap<>();
            
            for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                escapedParameterMap.put(entry.getKey(), getParameterValues(entry.getKey()));
            }
            
            return escapedParameterMap;
        }

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

Controller层

@RestController
@RequestMapping("/api/accounts")
@RequiredArgsConstructor
public class AccountController {
    private final AccountService accountService;
	
	@PostMapping
	public ResponseEntity<AccountDTO> createAccount(@Valid @RequestBody CreateAccountRequest request){
		AccountDTO account = accountService.createAccount(request);
        return ResponseEntity.ok(account);
	}

	@GetMapping("/{accountNumber}")
    public ResponseEntity<AccountDTO> getAccount(@PathVariable String accountNumber) {
        AccountDTO account = accountService.getAccount(accountNumber);
        return ResponseEntity.ok(account);
    }
    
    @PostMapping("/{accountNumber}/deposit")
    public ResponseEntity<AccountOperationResponse> deposit(
            @PathVariable String accountNumber,
            @RequestParam BigDecimal amount) {
        AccountOperationResponse response = accountService.deposit(accountNumber, amount);
        return ResponseEntity.ok(response);
    }
    
    @PostMapping("/{accountNumber}/withdraw")
    public ResponseEntity<AccountOperationResponse> withdraw(
            @PathVariable String accountNumber,
            @RequestParam BigDecimal amount) {
        AccountOperationResponse response = accountService.withdraw(accountNumber, amount);
        return ResponseEntity.ok(response);
    }
    
    @PostMapping("/{accountNumber}/freeze")
    public ResponseEntity<AccountOperationResponse> freezeAccount(@PathVariable String accountNumber) {
        AccountOperationResponse response = accountService.freezeAccount(accountNumber);
        return ResponseEntity.ok(response);
    }
    
    @PostMapping("/{accountNumber}/unfreeze")
    public ResponseEntity<AccountOperationResponse> unfreezeAccount(@PathVariable String accountNumber) {
        AccountOperationResponse response = accountService.unfreezeAccount(accountNumber);
        return ResponseEntity.ok(response);
    }
    
    @PostMapping("/{accountNumber}/close")
    public ResponseEntity<AccountOperationResponse> closeAccount(@PathVariable String accountNumber) {
        AccountOperationResponse response = accountService.closeAccount(accountNumber);
        return ResponseEntity.ok(response);
    }
}
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {
    private final AuthenticationService authenticationService;
    
    @PostMapping("/register/customer")
    public ResponseEntity<AuthenticationResponse> registerCustomer(
            @RequestBody AuthenticationRequest request
    ) {
        return ResponseEntity.ok(authenticationService.register(request, Role.CUSTOMER));
    }
    
    @PostMapping("/register/teller")
    public ResponseEntity<AuthenticationResponse> registerTeller(
            @RequestBody AuthenticationRequest request
    ) {
        return ResponseEntity.ok(authenticationService.register(request, Role.TELLER));
    }
    
    @PostMapping("/authenticate")
    public ResponseEntity<AuthenticationResponse> authenticate(
            @RequestBody AuthenticationRequest request
    ) {
        return ResponseEntity.ok(authenticationService.authenticate(request));
    }
}

Service层

@Service
@RequiredArgsConstructor
@Slf4j
public class AccountService{
	
	private final AccountRepository accountRepository;
    private final AccountNumberGenerator accountNumberGenerator;
	
	private final EncryptionUtil encryptionUtil;
    private final RateLimitConfig rateLimitConfig;
	
	@Transactional
	public AccountDTO createAccount(CreateAccountRequest request){
		
		String accountNumber = accountNumberGenerator.generate(request.getAccountType());
		Account account = new Account();
        account.setAccountNumber(accountNumber);
        account.setCustomerId(request.getCustomerId());
        account.setAccountType(request.getAccountType());
        account.setStatus(AccountStatus.ACTIVE);
        account.setCurrency(request.getCurrency());
        
        Account savedAccount = accountRepository.save(account);
        log.info("Account created: {}", accountNumber);
        
        return convertToDTO(savedAccount);
	}

	@Transactional(readOnly = true)
	@PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN') || "
            + "(hasRole('CUSTOMER') && @accountSecurityService.isAccountOwner(authentication, #accountNumber))")
    public AccountDTO getAccount(String accountNumber) {
		
		// 限流检查
        Bucket bucket = rateLimitConfig.bucket();
        if (!bucket.tryConsume(1)) {
            throw new AccountException(ErrorCode.TOO_MANY_REQUESTS);
        }
        Account account = accountRepository.findByAccountNumber(accountNumber)
                .orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));
        
        //敏感数据加密
        AccountDTO dto = convertToDto(account);
        dto.setAccountNumber(encryptionUtil.encrypt(dto.getAccountNumber()));
        return dto;
    }
    
    @Transactional
    @PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN')")
    public AccountOperationResponse deposit(String accountNumber, BigDecimal amount) {
        
        //解密账号
        String decryptedAccountNumber = encryptionUtil.decrypt(accountNumber);
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new AccountException(ErrorCode.INVALID_AMOUNT);
        }
        
        Account account = accountRepository.findByAccountNumberForUpdate(accountNumber)
                .orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));
        
        if (account.getStatus() != AccountStatus.ACTIVE) {
            throw new AccountException(ErrorCode.ACCOUNT_NOT_ACTIVE);
        }
        
        BigDecimal newBalance = account.getBalance().add(amount);
        account.setBalance(newBalance);
        account.setAvailableBalance(newBalance);
        
        accountRepository.save(account);
        log.info("Deposit {} to account {}", amount, accountNumber);
        
        // 审计日志
        logSecurityEvent("DEPOSIT", decryptedAccountNumber, amount);
        
        return buildSuccessResponse(accountNumber, newBalance, "Deposit successful");
    }
    
    @Transactional
    public AccountOperationResponse withdraw(String accountNumber, BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new AccountException(ErrorCode.INVALID_AMOUNT);
        }
        
        Account account = accountRepository.findByAccountNumberForUpdate(accountNumber)
                .orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));
        
        if (account.getStatus() != AccountStatus.ACTIVE) {
            throw new AccountException(ErrorCode.ACCOUNT_NOT_ACTIVE);
        }
        
        if (account.getAvailableBalance().compareTo(amount) < 0) {
            throw new AccountException(ErrorCode.INSUFFICIENT_BALANCE);
        }
        
        BigDecimal newBalance = account.getBalance().subtract(amount);
        account.setBalance(newBalance);
        account.setAvailableBalance(newBalance);
        
        accountRepository.save(account);
        log.info("Withdraw {} from account {}", amount, accountNumber);
        
        return buildSuccessResponse(accountNumber, newBalance, "Withdrawal successful");
    }
    
    @Transactional
    public AccountOperationResponse freezeAccount(String accountNumber) {
        Account account = accountRepository.findByAccountNumberForUpdate(accountNumber)
                .orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));
        
        if (account.getStatus() == AccountStatus.FROZEN) {
            throw new AccountException(ErrorCode.ACCOUNT_ALREADY_FROZEN);
        }
        
        account.setStatus(AccountStatus.FROZEN);
        accountRepository.save(account);
        log.info("Account {} frozen", accountNumber);
        
        return buildSuccessResponse(accountNumber, account.getBalance(), "Account frozen successfully");
    }
    
    @Transactional
    public AccountOperationResponse unfreezeAccount(String accountNumber) {
        Account account = accountRepository.findByAccountNumberForUpdate(accountNumber)
                .orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));
        
        if (account.getStatus() != AccountStatus.FROZEN) {
            throw new AccountException(ErrorCode.ACCOUNT_NOT_FROZEN);
        }
        
        account.setStatus(AccountStatus.ACTIVE);
        accountRepository.save(account);
        log.info("Account {} unfrozen", accountNumber);
        
        return buildSuccessResponse(accountNumber, account.getBalance(), "Account unfrozen successfully");
    }
    
    @Transactional
    public AccountOperationResponse closeAccount(String accountNumber) {
        Account account = accountRepository.findByAccountNumberForUpdate(accountNumber)
                .orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));
        
        if (account.getStatus() == AccountStatus.CLOSED) {
            throw new AccountException(ErrorCode.ACCOUNT_ALREADY_CLOSED);
        }
        
        if (account.getBalance().compareTo(BigDecimal.ZERO) != 0) {
            throw new AccountException(ErrorCode.ACCOUNT_BALANCE_NOT_ZERO);
        }
        
        account.setStatus(AccountStatus.CLOSED);
        accountRepository.save(account);
        log.info("Account {} closed", accountNumber);
        
        return buildSuccessResponse(accountNumber, account.getBalance(), "Account closed successfully");
    }
    
    private AccountDTO convertToDTO(Account account) {
        AccountDTO dto = new AccountDTO();
        dto.setId(account.getId());
        dto.setAccountNumber(account.getAccountNumber());
        dto.setCustomerId(account.getCustomerId());
        dto.setAccountType(account.getAccountType());
        dto.setStatus(account.getStatus());
        dto.setBalance(account.getBalance());
        dto.setAvailableBalance(account.getAvailableBalance());
        dto.setCurrency(account.getCurrency());
        dto.setCreatedAt(account.getCreatedAt());
        dto.setUpdatedAt(account.getUpdatedAt());
        return dto;
    }
    
    private AccountOperationResponse buildSuccessResponse(String accountNumber, BigDecimal newBalance, String message) {
        AccountOperationResponse response = new AccountOperationResponse();
        response.setSuccess(true);
        response.setMessage(message);
        response.setAccountNumber(accountNumber);
        response.setNewBalance(newBalance);
        return response;
    }
}
/**
	安全服务
*/
@Service
@RequiredArgsConstructor
public class AuthenticationService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtService jwtService;
    private final AuthenticationManager authenticationManager;
    
    public AuthenticationResponse register(AuthenticationRequest request, Role role) {
        var user = User.builder()
                .username(request.getUsername())
                .password(passwordEncoder.encode(request.getPassword()))
                .role(role)
                .build();
        userRepository.save(user);
        
        var jwtToken = jwtService.generateToken(user);
        var refreshToken = jwtService.generateRefreshToken(user);
        
        return AuthenticationResponse.builder()
                .token(jwtToken)
                .refreshToken(refreshToken)
                .build();
    }
    
    public AuthenticationResponse authenticate(AuthenticationRequest request) {
        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        request.getUsername(),
                        request.getPassword()
                )
        );
        
        var user = userRepository.findByUsername(request.getUsername())
                .orElseThrow();
        
        var jwtToken = jwtService.generateToken(user);
        var refreshToken = jwtService.generateRefreshToken(user);
        
        return AuthenticationResponse.builder()
                .token(jwtToken)
                .refreshToken(refreshToken)
                .build();
    }
}

Repository层

public interface AccountRepository extends JpaRepository<Account,Long>{
	
	Optional<Account> findByAccountNumber(String accountNumber);
	
	@Lock(LockModeType.PESSIMISTIC_WRITE)
	@Query("SELECT a FROM Account a WHERE a.accountNumber = :accountNumber")
    Optional<Account> findByAccountNumberForUpdate(@Param("accountNumber") String accountNumber);

	boolean existsByAccountNumber(String accountNumber);	
}

2. 交易处理

存款/取款

转账(同行/跨行)

批量交易处理

交易流水记录

交易限额管理

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

security:
  transaction:
    max-retry-attempts: 3
    retry-delay: 1000 # ms
@Entity
@Table(name = "transaction")
@Data
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String transactionId;
    
    @Column(nullable = false)
    private String accountNumber;
    
    @Column
    private String counterpartyAccountNumber;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private TransactionType transactionType;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private TransactionStatus status;//处理中、已完成、失败、已冲正、已取消
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal amount;
    
    @Column(precision = 19, scale = 4)
    private BigDecimal fee;
    
    @Column(nullable = false)
    private String currency = "CNY";
    
    @Column
    private String description;
    
    @Column(nullable = false)
    private String reference;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
    
    @Version
    private Long version;
}

@Entity
@Table(name = "transaction_limit")
@Data
public class TransactionLimit {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private TransactionType transactionType;//存款、取款、转账、账单支付、贷款还款、费用收取
    
    @Column(nullable = false)
    private String accountType;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal dailyLimit;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal perTransactionLimit;
    
    @Column(nullable = false)
    private Integer dailyCountLimit;
}
@Data
public class TransactionDTO {
    private String transactionId;
    private String accountNumber;
    private String counterpartyAccountNumber;
    private TransactionType transactionType;
    private TransactionStatus status;
    private BigDecimal amount;
    private BigDecimal fee;
    private String currency;
    private String description;
    private String reference;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
@Data
public class DepositRequest {
    @NotNull
    private String accountNumber;
    
    @NotNull
    @Positive
    private BigDecimal amount;
    
    private String description;
}

@Data
public class WithdrawalRequest {
    @NotNull
    private String accountNumber;
    
    @NotNull
    @Positive
    private BigDecimal amount;
    
    private String description;
}

@Data
public class TransferRequest {
    @NotNull
    private String fromAccountNumber;
    
    @NotNull
    private String toAccountNumber;
    
    @NotNull
    @Positive
    private BigDecimal amount;
    
    private String description;
}

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

异常

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

工具类

@Component
public class TransactionIdGenerator {
    private static final String BANK_CODE = "888";
    private final AtomicLong sequence = new AtomicLong(1);
    
    public String generate() {
        long timestamp = Instant.now().toEpochMilli();
        long seq = sequence.getAndIncrement();
        return String.format("%s-TRX-%d-%06d", BANK_CODE, timestamp, seq);
    }
}

Controller层

@Controller
@RequestMapping("/api/transactions")
@RequiredArgsConstructor
public class TransactionController{
	
	private final TransactionService transactionService;

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

	@PostMapping("/withdraw")
    public ResponseEntity<TransactionDTO> withdraw(@Valid @RequestBody WithdrawalRequest request) {
        return ResponseEntity.ok(transactionService.withdraw(request));
    }
    
    @PostMapping("/transfer")
    public ResponseEntity<TransactionDTO> transfer(@Valid @RequestBody TransferRequest request) {
        return ResponseEntity.ok(transactionService.transfer(request));
    }
    
    @PostMapping("/batch-transfer")
    @PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN')")
    public ResponseEntity<List<TransactionDTO>> batchTransfer(@Valid @RequestBody BatchTransactionRequest request) {
        return ResponseEntity.ok(transactionService.batchTransfer(request));
    }
    
    @GetMapping("/history")
    public ResponseEntity<List<TransactionDTO>> getTransactionHistory(
            @RequestParam String accountNumber,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
        return ResponseEntity.ok(transactionService.getTransactionHistory(accountNumber, startDate, endDate));
    }
	
}

Service层

@Service
@RequiredArgsConstructor
@Slf4j
public class TransactionService {
    private final TransactionRepository transactionRepository;
    private final TransactionLimitRepository limitRepository;
    private final AccountClientService accountClientService;
    private final TransactionIdGenerator idGenerator;
    private final EncryptionUtil encryptionUtil;
    
    @Transactional
    @PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN')")
    public TransactionDTO deposit(DepositRequest request) {
        // 解密账号
        String decryptedAccountNumber = encryptionUtil.decrypt(request.getAccountNumber());
        
        // 验证金额
        validateAmount(request.getAmount());
        
        // 检查账户状态
        checkAccountStatus(decryptedAccountNumber);
        
        // 执行存款
        BigDecimal newBalance = accountClientService.deposit(decryptedAccountNumber, request.getAmount());
        
        // 记录交易
        Transaction transaction = new Transaction();
        transaction.setTransactionId(idGenerator.generate());
        transaction.setAccountNumber(decryptedAccountNumber);
        transaction.setTransactionType(TransactionType.DEPOSIT);
        transaction.setStatus(TransactionStatus.COMPLETED);
        transaction.setAmount(request.getAmount());
        transaction.setCurrency("CNY");
        transaction.setDescription(request.getDescription());
        transaction.setReference("DEP-" + System.currentTimeMillis());
        
        Transaction saved = transactionRepository.save(transaction);
        log.info("Deposit completed: {}", saved.getTransactionId());
        
        return convertToDTO(saved);
    }
    
    @Transactional
    @PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN') || "
            + "(hasRole('CUSTOMER') && @accountSecurityService.isAccountOwner(authentication, #request.accountNumber))")
    public TransactionDTO withdraw(WithdrawalRequest request) {
        // 解密账号
        String decryptedAccountNumber = encryptionUtil.decrypt(request.getAccountNumber());
        
        // 验证金额
        validateAmount(request.getAmount());
        
        // 检查账户状态
        checkAccountStatus(decryptedAccountNumber);
        
        // 检查交易限额
        checkWithdrawalLimit(decryptedAccountNumber, request.getAmount());
        
        // 执行取款
        BigDecimal newBalance = accountClientService.withdraw(decryptedAccountNumber, request.getAmount());
        
        // 记录交易
        Transaction transaction = new Transaction();
        transaction.setTransactionId(idGenerator.generate());
        transaction.setAccountNumber(decryptedAccountNumber);
        transaction.setTransactionType(TransactionType.WITHDRAWAL);
        transaction.setStatus(TransactionStatus.COMPLETED);
        transaction.setAmount(request.getAmount().negate());
        transaction.setCurrency("CNY");
        transaction.setDescription(request.getDescription());
        transaction.setReference("WTH-" + System.currentTimeMillis());
        
        Transaction saved = transactionRepository.save(transaction);
        log.info("Withdrawal completed: {}", saved.getTransactionId());
        
        return convertToDTO(saved);
    }
    
    @Transactional
    @PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN') || "
            + "(hasRole('CUSTOMER') && @accountSecurityService.isAccountOwner(authentication, #request.fromAccountNumber))")
    public TransactionDTO transfer(TransferRequest request) {
        // 解密账号
        String decryptedFromAccount = encryptionUtil.decrypt(request.getFromAccountNumber());
        String decryptedToAccount = encryptionUtil.decrypt(request.getToAccountNumber());
        
        // 验证金额
        validateAmount(request.getAmount());
        
        // 检查账户状态
        checkAccountStatus(decryptedFromAccount);
        checkAccountStatus(decryptedToAccount);
        
        // 检查转账限额
        checkTransferLimit(decryptedFromAccount, request.getAmount());
        
        // 执行转账
        BigDecimal fromNewBalance = accountClientService.withdraw(decryptedFromAccount, request.getAmount());
        BigDecimal toNewBalance = accountClientService.deposit(decryptedToAccount, request.getAmount());
        
        // 记录交易(借方)
        Transaction debitTransaction = new Transaction();
        debitTransaction.setTransactionId(idGenerator.generate());
        debitTransaction.setAccountNumber(decryptedFromAccount);
        debitTransaction.setCounterpartyAccountNumber(decryptedToAccount);
        debitTransaction.setTransactionType(TransactionType.TRANSFER);
        debitTransaction.setStatus(TransactionStatus.COMPLETED);
        debitTransaction.setAmount(request.getAmount().negate());
        debitTransaction.setCurrency("CNY");
        debitTransaction.setDescription(request.getDescription());
        debitTransaction.setReference("TFR-DEBIT-" + System.currentTimeMillis());
        
        // 记录交易(贷方)
        Transaction creditTransaction = new Transaction();
        creditTransaction.setTransactionId(idGenerator.generate());
        creditTransaction.setAccountNumber(decryptedToAccount);
        creditTransaction.setCounterpartyAccountNumber(decryptedFromAccount);
        creditTransaction.setTransactionType(TransactionType.TRANSFER);
        creditTransaction.setStatus(TransactionStatus.COMPLETED);
        creditTransaction.setAmount(request.getAmount());
        creditTransaction.setCurrency("CNY");
        creditTransaction.setDescription(request.getDescription());
        creditTransaction.setReference("TFR-CREDIT-" + System.currentTimeMillis());
        
        transactionRepository.save(debitTransaction);
        transactionRepository.save(creditTransaction);
        log.info("Transfer completed: {} -> {}", debitTransaction.getTransactionId(), creditTransaction.getTransactionId());
        
        return convertToDTO(debitTransaction);
    }
    
    @Transactional
    @PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN')")
    public List<TransactionDTO> batchTransfer(BatchTransactionRequest request) {
        return request.getTransactions().stream()
                .map(this::transfer)
                .collect(Collectors.toList());
    }
    
    @Transactional(readOnly = true)
    @PreAuthorize("hasAnyRole('TELLER', 'MANAGER', 'ADMIN') || "
            + "(hasRole('CUSTOMER') && @accountSecurityService.isAccountOwner(authentication, #accountNumber))")
    public List<TransactionDTO> getTransactionHistory(String accountNumber, LocalDate startDate, LocalDate endDate) {
        String decryptedAccountNumber = encryptionUtil.decrypt(accountNumber);
        
        LocalDateTime start = startDate.atStartOfDay();
        LocalDateTime end = endDate.atTime(LocalTime.MAX);
        
        return transactionRepository
                .findByAccountNumberAndCreatedAtBetween(decryptedAccountNumber, start, end)
                .stream()
                .map(this::convertToDTO)
                .peek(dto -> dto.setAccountNumber(encryptionUtil.encrypt(dto.getAccountNumber())))
                .peek(dto -> {
                    if (dto.getCounterpartyAccountNumber() != null) {
                        dto.setCounterpartyAccountNumber(encryptionUtil.encrypt(dto.getCounterpartyAccountNumber()));
                    }
                })
                .collect(Collectors.toList());
    }
    
    private void validateAmount(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new AccountException(ErrorCode.INVALID_AMOUNT);
        }
    }
    
    private void checkAccountStatus(String accountNumber) {
        AccountDTO account = accountClientService.getAccount(accountNumber);
        if (account.getStatus() != AccountStatus.ACTIVE) {
            throw new AccountException(ErrorCode.ACCOUNT_NOT_ACTIVE);
        }
    }
    
    private void checkWithdrawalLimit(String accountNumber, BigDecimal amount) {
        AccountDTO account = accountClientService.getAccount(accountNumber);
        TransactionLimit limit = limitRepository.findByTransactionTypeAndAccountType(
                TransactionType.WITHDRAWAL, account.getAccountType().name())
                .orElseThrow(() -> new AccountException(ErrorCode.TRANSACTION_LIMIT_NOT_FOUND));
        
        // 检查单笔限额
        if (amount.compareTo(limit.getPerTransactionLimit()) > 0) {
            throw new AccountException(ErrorCode.EXCEED_PER_TRANSACTION_LIMIT);
        }
        
        // 检查当日累计限额
        LocalDateTime todayStart = LocalDate.now().atStartOfDay();
        LocalDateTime todayEnd = LocalDate.now().atTime(LocalTime.MAX);
        
        BigDecimal todayTotal = transactionRepository
                .findByAccountNumberAndCreatedAtBetween(accountNumber, todayStart, todayEnd)
                .stream()
                .filter(t -> t.getTransactionType() == TransactionType.WITHDRAWAL)
                .map(Transaction::getAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add)
                .abs();
        
        if (todayTotal.add(amount).compareTo(limit.getDailyLimit()) > 0) {
            throw new AccountException(ErrorCode.EXCEED_DAILY_LIMIT);
        }
        
        // 检查当日交易次数
        long todayCount = transactionRepository
                .findByAccountNumberAndCreatedAtBetween(accountNumber, todayStart, todayEnd)
                .stream()
                .filter(t -> t.getTransactionType() == TransactionType.WITHDRAWAL)
                .count();
        
        if (todayCount >= limit.getDailyCountLimit()) {
            throw new AccountException(ErrorCode.EXCEED_DAILY_COUNT_LIMIT);
        }
    }
    
    private void checkTransferLimit(String accountNumber, BigDecimal amount) {
        AccountDTO account = accountClientService.getAccount(accountNumber);
        TransactionLimit limit = limitRepository.findByTransactionTypeAndAccountType(
                TransactionType.TRANSFER, account.getAccountType().name())
                .orElseThrow(() -> new AccountException(ErrorCode.TRANSACTION_LIMIT_NOT_FOUND));
        
        // 检查单笔限额
        if (amount.compareTo(limit.getPerTransactionLimit()) > 0) {
            throw new AccountException(ErrorCode.EXCEED_PER_TRANSACTION_LIMIT);
        }
        
        // 检查当日累计限额
        LocalDateTime todayStart = LocalDate.now().atStartOfDay();
        LocalDateTime todayEnd = LocalDate.now().atTime(LocalTime.MAX);
        
        BigDecimal todayTotal = transactionRepository
                .findByAccountNumberAndCreatedAtBetween(accountNumber, todayStart, todayEnd)
                .stream()
                .filter(t -> t.getTransactionType() == TransactionType.TRANSFER)
                .map(Transaction::getAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add)
                .abs();
        
        if (todayTotal.add(amount).compareTo(limit.getDailyLimit()) > 0) {
            throw new AccountException(ErrorCode.EXCEED_DAILY_LIMIT);
        }
    }
    
    private TransactionDTO convertToDTO(Transaction transaction) {
        TransactionDTO dto = new TransactionDTO();
        dto.setTransactionId(transaction.getTransactionId());
        dto.setAccountNumber(transaction.getAccountNumber());
        dto.setCounterpartyAccountNumber(transaction.getCounterpartyAccountNumber());
        dto.setTransactionType(transaction.getTransactionType());
        dto.setStatus(transaction.getStatus());
        dto.setAmount(transaction.getAmount());
        dto.setFee(transaction.getFee());
        dto.setCurrency(transaction.getCurrency());
        dto.setDescription(transaction.getDescription());
        dto.setReference(transaction.getReference());
        dto.setCreatedAt(transaction.getCreatedAt());
        dto.setUpdatedAt(transaction.getUpdatedAt());
        return dto;
    }
}
@Service
@RequiredArgsConstructor
public class AccountClientService {
    private final RestTemplate restTemplate;
    private final EncryptionUtil encryptionUtil;
    
    @Value("${service.account.url}")
    private String accountServiceUrl;
    
    public AccountDTO getAccount(String accountNumber) {
        try {
            String encryptedAccountNumber = encryptionUtil.encrypt(accountNumber);
            
            HttpHeaders headers = new HttpHeaders();
            headers.set("X-Internal-Service", "transaction-service");
            
            ResponseEntity<AccountDTO> response = restTemplate.exchange(
                    accountServiceUrl + "/api/accounts/" + encryptedAccountNumber,
                    HttpMethod.GET,
                    new HttpEntity<>(headers),
                    AccountDTO.class);
            
            AccountDTO account = response.getBody();
            if (account != null) {
                account.setAccountNumber(accountNumber); // 返回解密后的账号
            }
            return account;
        } catch (HttpClientErrorException.NotFound e) {
            throw new AccountException(ErrorCode.ACCOUNT_NOT_FOUND);
        } catch (Exception e) {
            throw new AccountException(ErrorCode.ACCOUNT_SERVICE_UNAVAILABLE);
        }
    }
    
    public BigDecimal deposit(String accountNumber, BigDecimal amount) {
        try {
            String encryptedAccountNumber = encryptionUtil.encrypt(accountNumber);
            
            HttpHeaders headers = new HttpHeaders();
            headers.set("X-Internal-Service", "transaction-service");
            
            ResponseEntity<AccountOperationResponse> response = restTemplate.exchange(
                    accountServiceUrl + "/api/accounts/" + encryptedAccountNumber + "/deposit?amount=" + amount,
                    HttpMethod.POST,
                    new HttpEntity<>(headers),
                    AccountOperationResponse.class);
            
            AccountOperationResponse result = response.getBody();
            if (result == null || !result.isSuccess()) {
                throw new AccountException(ErrorCode.DEPOSIT_FAILED);
            }
            return result.getNewBalance();
        } catch (Exception e) {
            throw new AccountException(ErrorCode.ACCOUNT_SERVICE_UNAVAILABLE);
        }
    }
    
    public BigDecimal withdraw(String accountNumber, BigDecimal amount) {
        try {
            String encryptedAccountNumber = encryptionUtil.encrypt(accountNumber);
            
            HttpHeaders headers = new HttpHeaders();
            headers.set("X-Internal-Service", "transaction-service");
            
            ResponseEntity<AccountOperationResponse> response = restTemplate.exchange(
                    accountServiceUrl + "/api/accounts/" + encryptedAccountNumber + "/withdraw?amount=" + amount,
                    HttpMethod.POST,
                    new HttpEntity<>(headers),
                    AccountOperationResponse.class);
            
            AccountOperationResponse result = response.getBody();
            if (result == null || !result.isSuccess()) {
                throw new AccountException(ErrorCode.WITHDRAWAL_FAILED);
            }
            return result.getNewBalance();
        } catch (Exception e) {
            throw new AccountException(ErrorCode.ACCOUNT_SERVICE_UNAVAILABLE);
        }
    }
}

Repository层

public interface TransactionRepository extends JpaRepository<Transaction, Long> {
    Optional<Transaction> findByTransactionId(String transactionId);
    
    List<Transaction> findByAccountNumberAndCreatedAtBetween(
            String accountNumber, LocalDateTime startDate, LocalDateTime endDate);
    
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT t FROM Transaction t WHERE t.transactionId = :transactionId")
    Optional<Transaction> findByTransactionIdForUpdate(@Param("transactionId") String transactionId);
}
public interface TransactionLimitRepository extends JpaRepository<TransactionLimit, Long> {
    Optional<TransactionLimit> findByTransactionTypeAndAccountType(
            TransactionType transactionType, String accountType);
}

3. 支付结算

支付订单处理

清算对账

手续费计算

第三方支付对接

# 在原有配置基础上添加
payment:
  settlement:
    auto-enabled: true
    time: "02:00" # 自动结算时间
  third-party:
    timeout: 5000 # 第三方支付超时时间(ms)
    retry-times: 3 # 重试次数
@Entity
@Table(name = "payment_order")
@Data
public class PaymentOrder {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String orderNo;
    
    @Column(nullable = false)
    private String accountNumber;
    
    @Column(nullable = false)
    private String merchantCode;
    
    @Column(nullable = false)
    private String merchantOrderNo;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private PaymentOrderType orderType;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private PaymentOrderStatus status;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal amount;
    
    @Column(precision = 19, scale = 4)
    private BigDecimal fee;
    
    @Column(precision = 19, scale = 4)
    private BigDecimal settlementAmount;
    
    @Column(nullable = false)
    private String currency = "CNY";
    
    @Column
    private String description;
    
    @Column
    private String callbackUrl;
    
    @Column
    private String notifyUrl;
    
    @Column
    private String thirdPartyTransactionNo;
    
    @Column
    private LocalDateTime paymentTime;
    
    @Column
    private LocalDateTime settlementTime;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
    
    @Version
    private Long version;
}
@Entity
@Table(name = "settlement_record")
@Data
public class SettlementRecord {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String settlementNo;
    
    @Column(nullable = false)
    private String merchantCode;
    
    @Column(nullable = false)
    private LocalDate settlementDate;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal totalAmount;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal totalFee;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal netAmount;
    
    @Column(nullable = false)
    private Integer totalCount;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private SettlementStatus status;
    
    @Column
    private String bankTransactionNo;
    
    @Column
    private LocalDateTime completedTime;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

@Entity
@Table(name = "fee_config")
@Data
public class FeeConfig {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String merchantCode;
    
    @Column(nullable = false)
    private String paymentType;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private FeeCalculateMethod calculateMethod;
    
    @Column(precision = 19, scale = 4)
    private BigDecimal fixedFee;
    
    @Column(precision = 5, scale = 4)
    private BigDecimal rate;
    
    @Column(precision = 19, scale = 4)
    private BigDecimal minFee;
    
    @Column(precision = 19, scale = 4)
    private BigDecimal maxFee;
    
    @Column(nullable = false)
    private Boolean active = true;
}
public enum PaymentOrderType {
    WECHAT_PAY,     // 微信支付
    ALI_PAY,        // 支付宝
    UNION_PAY,      // 银联
    QUICK_PAY,      // 快捷支付
    BANK_TRANSFER   // 银行转账
}

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

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

public enum FeeCalculateMethod {
    FIXED,          // 固定费用
    PERCENTAGE,     // 百分比
    TIERED          // 阶梯费率
}
@Data
public class PaymentRequestDTO {
    @NotBlank
    private String accountNumber;
    
    @NotBlank
    private String merchantCode;
    
    @NotBlank
    private String merchantOrderNo;
    
    @NotNull
    private PaymentOrderType orderType;
    
    @NotNull
    @Positive
    private BigDecimal amount;
    
    @NotBlank
    private String currency;
    
    private String description;
    
    private String callbackUrl;
    
    private String notifyUrl;
}

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



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

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

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

工具类

@Component
public class OrderNoGenerator {
    private static final String BANK_CODE = "888";
    private final AtomicLong sequence = new AtomicLong(1);
    
    public String generate() {
        long timestamp = Instant.now().toEpochMilli();
        long seq = sequence.getAndIncrement();
        return String.format("%s-PAY-%d-%06d", BANK_CODE, timestamp, seq);
    }
}
@Component
public class SettlementNoGenerator {
    private static final String BANK_CODE = "888";
    private final AtomicLong sequence = new AtomicLong(1);
    
    public String generate() {
        String dateStr = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
        long seq = sequence.getAndIncrement();
        return String.format("%s-STL-%s-%06d", BANK_CODE, dateStr, seq);
    }
}

Controller层

@RestController
@RequestMapping("/api/payments")
@RequiredArgsConstructor
public class PaymentController {
    private final PaymentService paymentService;
    private final SettlementService settlementService;
    
    @PostMapping
    public PaymentResponseDTO createPayment(@Valid @RequestBody PaymentRequestDTO request) {
        return paymentService.createPayment(request);
    }
    
    @GetMapping("/{orderNo}")
    public PaymentResponseDTO queryPayment(@PathVariable String orderNo) {
        return paymentService.queryPayment(orderNo);
    }
    
    @GetMapping
    public PaymentResponseDTO queryPaymentByMerchant(
            @RequestParam String merchantCode,
            @RequestParam String merchantOrderNo) {
        return paymentService.queryPaymentByMerchant(merchantCode, merchantOrderNo);
    }
    
    @PostMapping("/settlements")
    public SettlementResponseDTO createSettlement(@Valid @RequestBody SettlementRequestDTO request) {
        return settlementService.createSettlement(request);
    }
    
    @GetMapping("/settlements/{settlementNo}")
    public SettlementResponseDTO querySettlement(@PathVariable String settlementNo) {
        return settlementService.querySettlement(settlementNo);
    }
}

Service层

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

	@Transactional
    public PaymentResponseDTO createPayment(PaymentRequestDTO request) {
        // 解密账号
        String decryptedAccountNumber = encryptionUtil.decrypt(request.getAccountNumber());
        
        // 检查是否已存在相同商户订单
        Optional<PaymentOrder> existingOrder = paymentOrderRepository
                .findByMerchantCodeAndMerchantOrderNo(request.getMerchantCode(), request.getMerchantOrderNo());
        if (existingOrder.isPresent()) {
            throw new AccountException(ErrorCode.DUPLICATE_MERCHANT_ORDER);
        }
        
        // 计算手续费
        BigDecimal fee = calculateFee(request.getMerchantCode(), request.getOrderType().name(), request.getAmount());
        BigDecimal settlementAmount = request.getAmount().subtract(fee);
        
        // 创建支付订单
        PaymentOrder order = new PaymentOrder();
        order.setOrderNo(orderNoGenerator.generate());
        order.setAccountNumber(decryptedAccountNumber);
        order.setMerchantCode(request.getMerchantCode());
        order.setMerchantOrderNo(request.getMerchantOrderNo());
        order.setOrderType(request.getOrderType());
        order.setStatus(PaymentOrderStatus.CREATED);
        order.setAmount(request.getAmount());
        order.setFee(fee);
        order.setSettlementAmount(settlementAmount);
        order.setCurrency(request.getCurrency());
        order.setDescription(request.getDescription());
        order.setCallbackUrl(request.getCallbackUrl());
        order.setNotifyUrl(request.getNotifyUrl());
        
        PaymentOrder savedOrder = paymentOrderRepository.save(order);
        log.info("Payment order created: {}", savedOrder.getOrderNo());
        
        // 异步处理支付
        processPaymentAsync(savedOrder.getOrderNo());
        
        return convertToPaymentResponse(savedOrder);
    }
    
    @Async
    public void processPaymentAsync(String orderNo) {
        try {
            PaymentOrder order = paymentOrderRepository.findByOrderNoForUpdate(orderNo)
                    .orElseThrow(() -> new AccountException(ErrorCode.ORDER_NOT_FOUND));
            
            if (order.getStatus() != PaymentOrderStatus.CREATED) {
                return;
            }
            
            order.setStatus(PaymentOrderStatus.PROCESSING);
            paymentOrderRepository.save(order);
            
            // 调用第三方支付
            ThirdPartyPaymentRequest paymentRequest = new ThirdPartyPaymentRequest();
            paymentRequest.setOrderNo(order.getOrderNo());
            paymentRequest.setAmount(order.getAmount());
            paymentRequest.setCurrency(order.getCurrency());
            paymentRequest.setAccountNumber(order.getAccountNumber());
            paymentRequest.setMerchantCode(order.getMerchantCode());
            paymentRequest.setPaymentType(order.getOrderType().name());
            
            ThirdPartyPaymentResponse paymentResponse = paymentGateway.processPayment(paymentRequest);
            
            if (paymentResponse.isSuccess()) {
                order.setStatus(PaymentOrderStatus.SUCCESS);
                order.setThirdPartyTransactionNo(paymentResponse.getTransactionNo());
                order.setPaymentTime(LocalDateTime.now());
                
                // 记录交易
                transactionService.withdraw(
                        order.getAccountNumber(),
                        order.getAmount(),
                        "Payment for order: " + order.getOrderNo());
            } else {
                order.setStatus(PaymentOrderStatus.FAILED);
                log.error("Payment failed for order {}: {}", orderNo, paymentResponse.getErrorMessage());
            }
            
            paymentOrderRepository.save(order);
            
            // 回调商户
            if (order.getCallbackUrl() != null) {
                notifyMerchant(order);
            }
            
        } catch (Exception e) {
            log.error("Error processing payment for order: " + orderNo, e);
            paymentOrderRepository.findByOrderNo(orderNo).ifPresent(order -> {
                order.setStatus(PaymentOrderStatus.FAILED);
                paymentOrderRepository.save(order);
            });
        }
    }
    
    @Transactional(readOnly = true)
    public PaymentResponseDTO queryPayment(String orderNo) {
        PaymentOrder order = paymentOrderRepository.findByOrderNo(orderNo)
                .orElseThrow(() -> new AccountException(ErrorCode.ORDER_NOT_FOUND));
        
        return convertToPaymentResponse(order);
    }
    
    @Transactional(readOnly = true)
    public PaymentResponseDTO queryPaymentByMerchant(String merchantCode, String merchantOrderNo) {
        PaymentOrder order = paymentOrderRepository.findByMerchantCodeAndMerchantOrderNo(merchantCode, merchantOrderNo)
                .orElseThrow(() -> new AccountException(ErrorCode.ORDER_NOT_FOUND));
        
        return convertToPaymentResponse(order);
    }
    
    private BigDecimal calculateFee(String merchantCode, String paymentType, BigDecimal amount) {
        FeeConfig feeConfig = feeConfigRepository.findByMerchantCodeAndPaymentType(merchantCode, paymentType)
                .orElseThrow(() -> new AccountException(ErrorCode.FEE_CONFIG_NOT_FOUND));
        
        switch (feeConfig.getCalculateMethod()) {
            case FIXED:
                return feeConfig.getFixedFee();
            case PERCENTAGE:
                BigDecimal fee = amount.multiply(feeConfig.getRate());
                if (feeConfig.getMinFee() != null && fee.compareTo(feeConfig.getMinFee()) < 0) {
                    return feeConfig.getMinFee();
                }
                if (feeConfig.getMaxFee() != null && fee.compareTo(feeConfig.getMaxFee()) > 0) {
                    return feeConfig.getMaxFee();
                }
                return fee;
            case TIERED:
                // 实现阶梯费率计算逻辑
                return feeConfig.getFixedFee(); // 简化处理
            default:
                return BigDecimal.ZERO;
        }
    }
    
    private void notifyMerchant(PaymentOrder order) {
        // 实现回调商户逻辑
        // 通常使用HTTP调用商户的callbackUrl或notifyUrl
        log.info("Notifying merchant for order: {}", order.getOrderNo());
    }
    
    private PaymentResponseDTO convertToPaymentResponse(PaymentOrder order) {
        PaymentResponseDTO response = new PaymentResponseDTO();
        response.setOrderNo(order.getOrderNo());
        response.setMerchantOrderNo(order.getMerchantOrderNo());
        response.setStatus(order.getStatus());
        response.setAmount(order.getAmount());
        response.setFee(order.getFee());
        response.setSettlementAmount(order.getSettlementAmount());
        response.setCurrency(order.getCurrency());
        response.setCreatedAt(order.getCreatedAt());
        
        // 加密账号
        response.setAccountNumber(encryptionUtil.encrypt(order.getAccountNumber()));
        
        return response;
    }
}
/**
	结算服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class SettlementService {
    private final PaymentOrderRepository paymentOrderRepository;
    private final SettlementRecordRepository settlementRepository;
    private final TransactionService transactionService;
    private final SettlementNoGenerator settlementNoGenerator;
    
    @Transactional
    public SettlementResponseDTO createSettlement(SettlementRequestDTO request) {
        LocalDate settlementDate = request.getSettlementDate() != null ? 
                request.getSettlementDate() : LocalDate.now().minusDays(1);
        
        // 检查是否已有结算记录
        Optional<SettlementRecord> existingSettlement = settlementRepository
                .findByMerchantCodeAndSettlementDate(request.getMerchantCode(), settlementDate);
        if (existingSettlement.isPresent()) {
            throw new AccountException(ErrorCode.SETTLEMENT_ALREADY_EXISTS);
        }
        
        // 查询待结算的支付订单
        List<PaymentOrder> orders = paymentOrderRepository
                .findByMerchantCodeAndStatusAndSettlementTimeIsNull(
                        request.getMerchantCode(), 
                        PaymentOrderStatus.SUCCESS);
        
        if (orders.isEmpty()) {
            throw new AccountException(ErrorCode.NO_ORDERS_TO_SETTLE);
        }
        
        // 计算结算金额
        BigDecimal totalAmount = orders.stream()
                .map(PaymentOrder::getSettlementAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        BigDecimal totalFee = orders.stream()
                .map(PaymentOrder::getFee)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        
        // 创建结算记录
        SettlementRecord settlement = new SettlementRecord();
        settlement.setSettlementNo(settlementNoGenerator.generate());
        settlement.setMerchantCode(request.getMerchantCode());
        settlement.setSettlementDate(settlementDate);
        settlement.setTotalAmount(totalAmount);
        settlement.setTotalFee(totalFee);
        settlement.setNetAmount(totalAmount);
        settlement.setTotalCount(orders.size());
        settlement.setStatus(SettlementStatus.PENDING);
        
        SettlementRecord savedSettlement = settlementRepository.save(settlement);
        log.info("Settlement record created: {}", savedSettlement.getSettlementNo());
        
        // 异步处理结算
        processSettlementAsync(savedSettlement.getSettlementNo());
        
        return convertToSettlementResponse(savedSettlement);
    }
    
    @Async
    public void processSettlementAsync(String settlementNo) {
        try {
            SettlementRecord settlement = settlementRepository.findBySettlementNo(settlementNo)
                    .orElseThrow(() -> new AccountException(ErrorCode.SETTLEMENT_NOT_FOUND));
            
            if (settlement.getStatus() != SettlementStatus.PENDING) {
                return;
            }
            
            settlement.setStatus(SettlementStatus.PROCESSING);
            settlementRepository.save(settlement);
            
            // 执行资金划拨
            transactionService.deposit(
                    getMerchantAccount(settlement.getMerchantCode()),
                    settlement.getNetAmount(),
                    "Settlement for " + settlement.getSettlementDate());
            
            // 更新支付订单结算状态
            List<PaymentOrder> orders = paymentOrderRepository
                    .findByMerchantCodeAndStatusAndSettlementTimeIsNull(
                            settlement.getMerchantCode(), 
                            PaymentOrderStatus.SUCCESS);
            
            orders.forEach(order -> {
                order.setSettlementTime(LocalDateTime.now());
                paymentOrderRepository.save(order);
            });
            
            settlement.setStatus(SettlementStatus.COMPLETED);
            settlement.setCompletedTime(LocalDateTime.now());
            settlementRepository.save(settlement);
            
            log.info("Settlement completed: {}", settlementNo);
            
        } catch (Exception e) {
            log.error("Error processing settlement: " + settlementNo, e);
            settlementRepository.findBySettlementNo(settlementNo).ifPresent(s -> {
                s.setStatus(SettlementStatus.FAILED);
                settlementRepository.save(s);
            });
        }
    }
    
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
    public void autoSettlement() {
        LocalDate settlementDate = LocalDate.now().minusDays(1);
        log.info("Starting auto settlement for date: {}", settlementDate);
        
        // 获取所有需要结算的商户
        List<String> merchantCodes = paymentOrderRepository
                .findDistinctMerchantCodeByStatusAndSettlementTimeIsNull(PaymentOrderStatus.SUCCESS);
        
        merchantCodes.forEach(merchantCode -> {
            try {
                SettlementRequestDTO request = new SettlementRequestDTO();
                request.setMerchantCode(merchantCode);
                request.setSettlementDate(settlementDate);
                createSettlement(request);
            } catch (Exception e) {
                log.error("Auto settlement failed for merchant: " + merchantCode, e);
            }
        });
    }
    
    private String getMerchantAccount(String merchantCode) {
        // 实际项目中应根据商户编码查询商户的结算账户
        return "MERCHANT_" + merchantCode;
    }
    
    private SettlementResponseDTO convertToSettlementResponse(SettlementRecord settlement) {
        SettlementResponseDTO response = new SettlementResponseDTO();
        response.setSettlementNo(settlement.getSettlementNo());
        response.setMerchantCode(settlement.getMerchantCode());
        response.setSettlementDate(settlement.getSettlementDate());
        response.setTotalAmount(settlement.getTotalAmount());
        response.setTotalFee(settlement.getTotalFee());
        response.setNetAmount(settlement.getNetAmount());
        response.setTotalCount(settlement.getTotalCount());
        response.setStatus(settlement.getStatus());
        response.setCompletedTime(settlement.getCompletedTime());
        return response;
    }
}
/**
	第三方支付对接
*/
@Component
public class ThirdPartyPaymentGateway {
    public ThirdPartyPaymentResponse processPayment(ThirdPartyPaymentRequest request) {
        // 实际项目中这里会调用第三方支付平台的API
        // 以下是模拟实现
        
        ThirdPartyPaymentResponse response = new ThirdPartyPaymentResponse();
        
        try {
            // 模拟支付处理
            Thread.sleep(500);
            
            // 模拟90%成功率
            if (Math.random() > 0.1) {
                response.setSuccess(true);
                response.setTransactionNo("TP" + System.currentTimeMillis());
                response.setPaymentUrl("https://payment-gateway.com/pay/" + request.getOrderNo());
            } else {
                response.setSuccess(false);
                response.setErrorCode("PAYMENT_FAILED");
                response.setErrorMessage("Payment processing failed");
            }
        } catch (Exception e) {
            response.setSuccess(false);
            response.setErrorCode("SYSTEM_ERROR");
            response.setErrorMessage(e.getMessage());
        }
        
        return response;
    }
}

Repository层

public interface PaymentOrderRepository extends JpaRepository<PaymentOrder, Long> {
    Optional<PaymentOrder> findByOrderNo(String orderNo);
    
    Optional<PaymentOrder> findByMerchantCodeAndMerchantOrderNo(String merchantCode, String merchantOrderNo);
    
    List<PaymentOrder> findByMerchantCodeAndStatusAndSettlementTimeIsNull(String merchantCode, PaymentOrderStatus status);
    
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM PaymentOrder p WHERE p.orderNo = :orderNo")
    Optional<PaymentOrder> findByOrderNoForUpdate(@Param("orderNo") String orderNo);
}
public interface SettlementRecordRepository extends JpaRepository<SettlementRecord, Long> {
    Optional<SettlementRecord> findBySettlementNo(String settlementNo);
    
    Optional<SettlementRecord> findByMerchantCodeAndSettlementDate(String merchantCode, LocalDate settlementDate);
    
    List<SettlementRecord> findBySettlementDateAndStatus(LocalDate settlementDate, SettlementStatus status);
}
public interface FeeConfigRepository extends JpaRepository<FeeConfig, Long> {
    Optional<FeeConfig> findByMerchantCodeAndPaymentType(String merchantCode, String paymentType);
    
    List<FeeConfig> findByMerchantCode(String merchantCode);
}

4. 贷款管理

贷款申请审批

贷款发放

还款计划

逾期管理

利率调整

@Entity
@Table(name = "loan_application")
@Data
public class LoanApplication {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String applicationNo;
    
    @Column(nullable = false)
    private Long customerId;
    
    @Column(nullable = false)
    private String accountNumber;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private LoanType loanType;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal amount;
    
    @Column(nullable = false)
    private Integer term; // in months
    
    @Column(nullable = false, precision = 5, scale = 4)
    private BigDecimal interestRate;
    
    @Column(nullable = false)
    private String purpose;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private LoanStatus status;
    
    @Column
    private Long approvedBy;
    
    @Column
    private LocalDateTime approvedAt;
    
    @Column
    private String rejectionReason;
    
    @Column
    private LocalDate disbursementDate;
    
    @Column
    private LocalDate maturityDate;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
    
    @Version
    private Long version;
}

@Entity
@Table(name = "loan_account")
@Data
public class LoanAccount {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String loanAccountNo;
    
    @Column(nullable = false)
    private String applicationNo;
    
    @Column(nullable = false)
    private Long customerId;
    
    @Column(nullable = false)
    private String accountNumber;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal originalAmount;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal outstandingAmount;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal interestAccrued;
    
    @Column(nullable = false, precision = 5, scale = 4)
    private BigDecimal interestRate;
    
    @Column(nullable = false)
    private LocalDate startDate;
    
    @Column(nullable = false)
    private LocalDate maturityDate;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private LoanAccountStatus status;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
    
    @Version
    private Long version;
}

@Entity
@Table(name = "repayment_schedule")
@Data
public class RepaymentSchedule {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String loanAccountNo;
    
    @Column(nullable = false)
    private Integer installmentNo;
    
    @Column(nullable = false)
    private LocalDate dueDate;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal principalAmount;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal interestAmount;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal totalAmount;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal outstandingPrincipal;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private RepaymentStatus status;
    
    @Column
    private LocalDate paidDate;
    
    @Column(precision = 19, scale = 4)
    private BigDecimal paidAmount;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

@Entity
@Table(name = "loan_product")
@Data
public class LoanProduct {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String productCode;
    
    @Column(nullable = false)
    private String productName;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private LoanType loanType;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal minAmount;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal maxAmount;
    
    @Column(nullable = false)
    private Integer minTerm; // in months
    
    @Column(nullable = false)
    private Integer maxTerm; // in months
    
    @Column(nullable = false, precision = 5, scale = 4)
    private BigDecimal baseInterestRate;
    
    @Column(nullable = false)
    private Boolean active = true;
}
public enum LoanType {
    PERSONAL_LOAN,      // 个人贷款
    MORTGAGE_LOAN,      // 抵押贷款
    AUTO_LOAN,          // 汽车贷款
    BUSINESS_LOAN,      // 商业贷款
    CREDIT_LINE         // 信用额度
}

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

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

public enum RepaymentStatus {
    PENDING,            // 待还款
    PAID,               // 已还款
    PARTIALLY_PAID,     // 部分还款
    OVERDUE,            // 逾期
    WAIVED              // 已豁免
}
@Data
public class LoanApplicationDTO {
    private String applicationNo;
    private Long customerId;
    private String accountNumber;
    private LoanType loanType;
    private BigDecimal amount;
    private Integer term;
    private BigDecimal interestRate;
    private String purpose;
    private LoanStatus status;
    private Long approvedBy;
    private LocalDateTime approvedAt;
    private String rejectionReason;
    private LocalDate disbursementDate;
    private LocalDate maturityDate;
    private LocalDateTime createdAt;
}


@Data
public class LoanApplicationRequest {
    @NotNull
    private Long customerId;
    
    @NotBlank
    private String accountNumber;
    
    @NotNull
    private LoanType loanType;
    
    @NotNull
    @DecimalMin("1000.00")
    private BigDecimal amount;
    
    @NotNull
    @Min(1)
    @Max(360)
    private Integer term;
    
    @NotBlank
    @Size(min = 10, max = 500)
    private String purpose;
}

@Data
public class LoanApprovalRequest {
    @NotNull
    private Boolean approved;
    
    private String comments;
}
@Data
public class LoanAccountDTO {
    private String loanAccountNo;
    private String applicationNo;
    private Long customerId;
    private String accountNumber;
    private BigDecimal originalAmount;
    private BigDecimal outstandingAmount;
    private BigDecimal interestAccrued;
    private BigDecimal interestRate;
    private LocalDate startDate;
    private LocalDate maturityDate;
    private LoanAccountStatus status;
}
@Data
public class RepaymentDTO {
    private Long id;
    private String loanAccountNo;
    private Integer installmentNo;
    private LocalDate dueDate;
    private BigDecimal principalAmount;
    private BigDecimal interestAmount;
    private BigDecimal totalAmount;
    private BigDecimal outstandingPrincipal;
    private RepaymentStatus status;
    private LocalDate paidDate;
    private BigDecimal paidAmount;
}
@Data
public class RepaymentRequest {
    @NotNull
    @DecimalMin("0.01")
    private BigDecimal amount;
    
    @NotBlank
    private String transactionReference;
}

工具类

@Component
public class ApplicationNoGenerator {
    private static final String BANK_CODE = "888";
    private final AtomicLong sequence = new AtomicLong(1);
    
    public String generate() {
        long timestamp = Instant.now().toEpochMilli();
        long seq = sequence.getAndIncrement();
        return String.format("%s-LN-%d-%06d", BANK_CODE, timestamp, seq);
    }
}
@Component
public class LoanAccountNoGenerator {
    private static final String BANK_CODE = "888";
    private final AtomicLong sequence = new AtomicLong(1);
    
    public String generate() {
        String dateStr = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
        long seq = sequence.getAndIncrement();
        return String.format("%s-LA-%s-%06d", BANK_CODE, dateStr, seq);
    }
}

Controller层

@RestController
@RequestMapping("/api/loan-applications")
@RequiredArgsConstructor
public class LoanApplicationController {
    private final LoanApplicationService applicationService;
    
    @PostMapping
    public LoanApplicationDTO createApplication(@Valid @RequestBody LoanApplicationRequest request) {
        return applicationService.createApplication(request);
    }
    
    @PutMapping("/{applicationNo}/approval")
    @PreAuthorize("hasAnyRole('LOAN_OFFICER', 'MANAGER', 'ADMIN')")
    public LoanApplicationDTO approveApplication(
            @PathVariable String applicationNo,
            @Valid @RequestBody LoanApprovalRequest request) {
        return applicationService.approveApplication(applicationNo, request);
    }
    
    @GetMapping("/customer/{customerId}")
    public List<LoanApplicationDTO> getCustomerApplications(@PathVariable Long customerId) {
        return applicationService.getCustomerApplications(customerId);
    }
    
    @GetMapping
    @PreAuthorize("hasAnyRole('LOAN_OFFICER', 'MANAGER', 'ADMIN')")
    public List<LoanApplicationDTO> getApplicationsByStatus(@RequestParam String status) {
        return applicationService.getApplicationsByStatus(LoanStatus.valueOf(status));
    }
}
@RestController
@RequestMapping("/api/loan-accounts")
@RequiredArgsConstructor
public class LoanAccountController {
    private final LoanAccountService loanAccountService;
    
    @GetMapping("/{loanAccountNo}")
    public LoanAccountDTO getLoanAccount(@PathVariable String loanAccountNo) {
        return loanAccountService.getLoanAccount(loanAccountNo);
    }
    
    @GetMapping("/customer/{customerId}")
    public List<LoanAccountDTO> getCustomerLoanAccounts(@PathVariable Long customerId) {
        return loanAccountService.getCustomerLoanAccounts(customerId);
    }
    
    @GetMapping("/{loanAccountNo}/repayments")
    public List<RepaymentDTO> getRepaymentSchedule(@PathVariable String loanAccountNo) {
        return loanAccountService.getRepaymentSchedule(loanAccountNo);
    }
    
    @PostMapping("/{loanAccountNo}/repayments")
    public RepaymentDTO makeRepayment(
            @PathVariable String loanAccountNo,
            @Valid @RequestBody RepaymentRequest request) {
        return loanAccountService.makeRepayment(loanAccountNo, request);
    }
}

Service层

@Service
@RequiredArgsConstructor
@Slf4j
public class LoanApplicationService {
    private final LoanApplicationRepository applicationRepository;
    private final LoanProductRepository productRepository;
    private final ApplicationNoGenerator applicationNoGenerator;
    private final EncryptionUtil encryptionUtil;
	
	@Transactional
    public LoanApplicationDTO createApplication(LoanApplicationRequest request) {
        // 解密账号
        String decryptedAccountNumber = encryptionUtil.decrypt(request.getAccountNumber());
        
        // 验证贷款产品
        LoanProduct product = productRepository.findByLoanType(request.getLoanType())
                .orElseThrow(() -> new AccountException(ErrorCode.LOAN_PRODUCT_NOT_FOUND));
        
        // 验证贷款金额和期限
        validateLoanAmountAndTerm(request.getAmount(), request.getTerm(), product);
        
        // 计算利率 (简化处理,实际业务中可能有更复杂的利率计算逻辑)
        BigDecimal interestRate = calculateInterestRate(request.getLoanType(), request.getAmount(), request.getTerm());
        
        // 创建贷款申请
        LoanApplication application = new LoanApplication();
        application.setApplicationNo(applicationNoGenerator.generate());
        application.setCustomerId(request.getCustomerId());
        application.setAccountNumber(decryptedAccountNumber);
        application.setLoanType(request.getLoanType());
        application.setAmount(request.getAmount());
        application.setTerm(request.getTerm());
        application.setInterestRate(interestRate);
        application.setPurpose(request.getPurpose());
        application.setStatus(LoanStatus.PENDING);
        
        LoanApplication saved = applicationRepository.save(application);
        log.info("Loan application created: {}", saved.getApplicationNo());
        
        return convertToDTO(saved);
    }
    
    @Transactional
    @PreAuthorize("hasAnyRole('LOAN_OFFICER', 'MANAGER', 'ADMIN')")
    public LoanApplicationDTO approveApplication(String applicationNo, LoanApprovalRequest request) {
        LoanApplication application = applicationRepository.findByApplicationNo(applicationNo)
                .orElseThrow(() -> new AccountException(ErrorCode.APPLICATION_NOT_FOUND));
        
        if (application.getStatus() != LoanStatus.PENDING) {
            throw new AccountException(ErrorCode.APPLICATION_NOT_PENDING);
        }
        
        if (request.getApproved()) {
            application.setStatus(LoanStatus.APPROVED);
            application.setApprovedBy(getCurrentUserId());
            application.setApprovedAt(LocalDateTime.now());
            application.setMaturityDate(calculateMaturityDate(application.getCreatedAt().toLocalDate(), application.getTerm()));
            
            // 设置预计发放日期(3个工作日后)
            application.setDisbursementDate(calculateDisbursementDate(LocalDate.now()));
        } else {
            application.setStatus(LoanStatus.REJECTED);
            application.setRejectionReason(request.getComments());
        }
        
        LoanApplication saved = applicationRepository.save(application);
        log.info("Loan application {}: {}", request.getApproved() ? "approved" : "rejected", applicationNo);
        
        return convertToDTO(saved);
    }
    
    @Transactional(readOnly = true)
    public List<LoanApplicationDTO> getCustomerApplications(Long customerId) {
        return applicationRepository.findByCustomerId(customerId)
                .stream()
                .map(this::convertToDTO)
                .peek(dto -> dto.setAccountNumber(encryptionUtil.encrypt(dto.getAccountNumber())))
                .collect(Collectors.toList());
    }
    
    @Transactional(readOnly = true)
    @PreAuthorize("hasAnyRole('LOAN_OFFICER', 'MANAGER', 'ADMIN')")
    public List<LoanApplicationDTO> getApplicationsByStatus(LoanStatus status) {
        return applicationRepository.findByStatus(status)
                .stream()
                .map(this::convertToDTO)
                .peek(dto -> dto.setAccountNumber(encryptionUtil.encrypt(dto.getAccountNumber())))
                .collect(Collectors.toList());
    }
    
    @Scheduled(cron = "0 0 9 * * ?") // 每天上午9点执行
    public void processApprovedLoans() {
        LocalDate today = LocalDate.now();
        List<LoanApplication> applications = applicationRepository
                .findPendingDisbursement(LoanStatus.APPROVED, today);
        
        applications.forEach(application -> {
            try {
                disburseLoan(application.getApplicationNo());
            } catch (Exception e) {
                log.error("Failed to disburse loan: " + application.getApplicationNo(), e);
            }
        });
    }
    
    @Transactional
    public void disburseLoan(String applicationNo) {
        LoanApplication application = applicationRepository.findByApplicationNo(applicationNo)
                .orElseThrow(() -> new AccountException(ErrorCode.APPLICATION_NOT_FOUND));
        
        if (application.getStatus() != LoanStatus.APPROVED) {
            throw new AccountException(ErrorCode.APPLICATION_NOT_APPROVED);
        }
        
        if (application.getDisbursementDate().isAfter(LocalDate.now())) {
            throw new AccountException(ErrorCode.DISBURSEMENT_DATE_NOT_REACHED);
        }
        
        // 创建贷款账户
        LoanAccountDTO loanAccount = createLoanAccount(application);
        
        // 标记贷款申请为已发放
        application.setStatus(LoanStatus.DISBURSED);
        applicationRepository.save(application);
        
        log.info("Loan disbursed: {}", applicationNo);
    }
    
    private LoanAccountDTO createLoanAccount(LoanApplication application) {
        // 实际实现会调用LoanAccountService创建贷款账户
        // 这里简化处理
        return new LoanAccountDTO();
    }
    
    private void validateLoanAmountAndTerm(BigDecimal amount, Integer term, LoanProduct product) {
        if (amount.compareTo(product.getMinAmount()) < 0 || amount.compareTo(product.getMaxAmount()) > 0) {
            throw new AccountException(ErrorCode.INVALID_LOAN_AMOUNT);
        }
        
        if (term < product.getMinTerm() || term > product.getMaxTerm()) {
            throw new AccountException(ErrorCode.INVALID_LOAN_TERM);
        }
    }
    
    private BigDecimal calculateInterestRate(LoanType loanType, BigDecimal amount, Integer term) {
        // 简化处理,实际业务中可能有更复杂的利率计算逻辑
        return BigDecimal.valueOf(0.08); // 8%
    }
    
    private LocalDate calculateMaturityDate(LocalDate startDate, Integer termMonths) {
        return startDate.plusMonths(termMonths);
    }
    
    private LocalDate calculateDisbursementDate(LocalDate today) {
        // 简化处理,实际业务中可能需要考虑工作日
        return today.plusDays(3);
    }
    
    private Long getCurrentUserId() {
        // 从安全上下文中获取当前用户ID
        return 1L; // 简化处理
    }
    
    private LoanApplicationDTO convertToDTO(LoanApplication application) {
        LoanApplicationDTO dto = new LoanApplicationDTO();
        dto.setApplicationNo(application.getApplicationNo());
        dto.setCustomerId(application.getCustomerId());
        dto.setAccountNumber(application.getAccountNumber());
        dto.setLoanType(application.getLoanType());
        dto.setAmount(application.getAmount());
        dto.setTerm(application.getTerm());
        dto.setInterestRate(application.getInterestRate());
        dto.setPurpose(application.getPurpose());
        dto.setStatus(application.getStatus());
        dto.setApprovedBy(application.getApprovedBy());
        dto.setApprovedAt(application.getApprovedAt());
        dto.setRejectionReason(application.getRejectionReason());
        dto.setDisbursementDate(application.getDisbursementDate());
        dto.setMaturityDate(application.getMaturityDate());
        dto.setCreatedAt(application.getCreatedAt());
        return dto;
    }
}
/**
	贷款账户服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class LoanAccountService {
    private final LoanAccountRepository loanAccountRepository;
    private final LoanApplicationRepository applicationRepository;
    private final RepaymentScheduleRepository repaymentRepository;
    private final TransactionService transactionService;
    private final LoanAccountNoGenerator accountNoGenerator;
    
    @Transactional
    public LoanAccountDTO createLoanAccount(LoanApplication application) {
        // 生成还款计划
        List<RepaymentSchedule> repaymentSchedules = generateRepaymentSchedule(application);
        
        // 创建贷款账户
        LoanAccount loanAccount = new LoanAccount();
        loanAccount.setLoanAccountNo(accountNoGenerator.generate());
        loanAccount.setApplicationNo(application.getApplicationNo());
        loanAccount.setCustomerId(application.getCustomerId());
        loanAccount.setAccountNumber(application.getAccountNumber());
        loanAccount.setOriginalAmount(application.getAmount());
        loanAccount.setOutstandingAmount(application.getAmount());
        loanAccount.setInterestAccrued(BigDecimal.ZERO);
        loanAccount.setInterestRate(application.getInterestRate());
        loanAccount.setStartDate(LocalDate.now());
        loanAccount.setMaturityDate(application.getMaturityDate());
        loanAccount.setStatus(LoanAccountStatus.ACTIVE);
        
        LoanAccount savedAccount = loanAccountRepository.save(loanAccount);
        
        // 保存还款计划
        repaymentSchedules.forEach(schedule -> schedule.setLoanAccountNo(savedAccount.getLoanAccountNo()));
        repaymentRepository.saveAll(repaymentSchedules);
        
        // 发放贷款资金
        transactionService.deposit(
                application.getAccountNumber(),
                application.getAmount(),
                "Loan disbursement for " + savedAccount.getLoanAccountNo());
        
        log.info("Loan account created: {}", savedAccount.getLoanAccountNo());
        
        return convertToDTO(savedAccount);
    }
    
    @Transactional
    public RepaymentDTO makeRepayment(String loanAccountNo, RepaymentRequest request) {
        LoanAccount loanAccount = loanAccountRepository.findByLoanAccountNoForUpdate(loanAccountNo)
                .orElseThrow(() -> new AccountException(ErrorCode.LOAN_ACCOUNT_NOT_FOUND));
        
        if (loanAccount.getStatus() != LoanAccountStatus.ACTIVE && 
            loanAccount.getStatus() != LoanAccountStatus.DELINQUENT) {
            throw new AccountException(ErrorCode.LOAN_ACCOUNT_NOT_ACTIVE);
        }
        
        // 查找到期的还款计划
        List<RepaymentSchedule> dueInstallments = repaymentRepository
                .findDueInstallments(
                        loanAccountNo, 
                        LocalDate.now(), 
                        List.of(RepaymentStatus.PENDING, RepaymentStatus.OVERDUE));
        
        if (dueInstallments.isEmpty()) {
            throw new AccountException(ErrorCode.NO_DUE_INSTALLMENTS);
        }
        
        // 处理还款
        BigDecimal remainingAmount = request.getAmount();
        for (RepaymentSchedule installment : dueInstallments) {
            if (remainingAmount.compareTo(BigDecimal.ZERO) <= 0) {
                break;
            }
            
            BigDecimal amountToPay = installment.getTotalAmount().subtract(installment.getPaidAmount() != null ? 
                    installment.getPaidAmount() : BigDecimal.ZERO);
            BigDecimal paymentAmount = remainingAmount.compareTo(amountToPay) >= 0 ? 
                    amountToPay : remainingAmount;
            
            // 记录还款
            installment.setPaidAmount((installment.getPaidAmount() != null ? 
                    installment.getPaidAmount() : BigDecimal.ZERO).add(paymentAmount));
            
            if (installment.getPaidAmount().compareTo(installment.getTotalAmount()) >= 0) {
                installment.setStatus(RepaymentStatus.PAID);
                installment.setPaidDate(LocalDate.now());
            } else {
                installment.setStatus(RepaymentStatus.PARTIALLY_PAID);
            }
            
            repaymentRepository.save(installment);
            remainingAmount = remainingAmount.subtract(paymentAmount);
            
            // 更新贷款账户余额
            BigDecimal principalPaid = paymentAmount.multiply(
                    installment.getPrincipalAmount().divide(installment.getTotalAmount(), 4, BigDecimal.ROUND_HALF_UP));
            
            loanAccount.setOutstandingAmount(loanAccount.getOutstandingAmount().subtract(principalPaid));
            loanAccount.setInterestAccrued(loanAccount.getInterestAccrued().subtract(
                    paymentAmount.subtract(principalPaid)));
        }
        
        // 保存贷款账户更新
        loanAccountRepository.save(loanAccount);
        
        // 记录交易
        transactionService.withdraw(
                loanAccount.getAccountNumber(),
                request.getAmount(),
                "Loan repayment for " + loanAccountNo + ", Ref: " + request.getTransactionReference());
        
        log.info("Repayment received for loan account: {}, amount: {}", loanAccountNo, request.getAmount());
        
        return convertToDTO(dueInstallments.get(0));
    }
    
    @Scheduled(cron = "0 0 0 * * ?") // 每天午夜执行
    public void checkOverdueLoans() {
        LocalDate today = LocalDate.now();
        List<LoanAccount> dueLoans = loanAccountRepository.findDueLoans(
                List.of(LoanAccountStatus.ACTIVE, LoanAccountStatus.DELINQUENT), today);
        
        dueLoans.forEach(loan -> {
            try {
                updateOverdueStatus(loan.getLoanAccountNo(), today);
            } catch (Exception e) {
                log.error("Failed to update overdue status for loan: " + loan.getLoanAccountNo(), e);
            }
        });
    }
    
    @Transactional
    public void updateOverdueStatus(String loanAccountNo, LocalDate asOfDate) {
        LoanAccount loanAccount = loanAccountRepository.findByLoanAccountNoForUpdate(loanAccountNo)
                .orElseThrow(() -> new AccountException(ErrorCode.LOAN_ACCOUNT_NOT_FOUND));
        
        // 查找逾期的还款计划
        List<RepaymentSchedule> overdueInstallments = repaymentRepository
                .findDueInstallments(
                        loanAccountNo, 
                        asOfDate, 
                        List.of(RepaymentStatus.PENDING));
        
        if (!overdueInstallments.isEmpty()) {
            overdueInstallments.forEach(installment -> {
                installment.setStatus(RepaymentStatus.OVERDUE);
                repaymentRepository.save(installment);
            });
            
            loanAccount.setStatus(LoanAccountStatus.DELINQUENT);
            loanAccountRepository.save(loanAccount);
            
            log.info("Loan account marked as delinquent: {}", loanAccountNo);
        }
    }
    
    private List<RepaymentSchedule> generateRepaymentSchedule(LoanApplication application) {
        // 简化处理,生成等额本息还款计划
        BigDecimal monthlyRate = application.getInterestRate().divide(BigDecimal.valueOf(12), 6, BigDecimal.ROUND_HALF_UP);
        BigDecimal monthlyPayment = calculateMonthlyPayment(application.getAmount(), monthlyRate, application.getTerm());
        
        LocalDate paymentDate = LocalDate.now().plusMonths(1);
        BigDecimal remainingPrincipal = application.getAmount();
        
        List<RepaymentSchedule> schedules = new ArrayList<>();
        
        for (int i = 1; i <= application.getTerm(); i++) {
            BigDecimal interest = remainingPrincipal.multiply(monthlyRate);
            BigDecimal principal = monthlyPayment.subtract(interest);
            
            if (i == application.getTerm()) {
                principal = remainingPrincipal;
            }
            
            RepaymentSchedule schedule = new RepaymentSchedule();
            schedule.setInstallmentNo(i);
            schedule.setDueDate(paymentDate);
            schedule.setPrincipalAmount(principal);
            schedule.setInterestAmount(interest);
            schedule.setTotalAmount(principal.add(interest));
            schedule.setOutstandingPrincipal(remainingPrincipal);
            schedule.setStatus(RepaymentStatus.PENDING);
            
            schedules.add(schedule);
            
            remainingPrincipal = remainingPrincipal.subtract(principal);
            paymentDate = paymentDate.plusMonths(1);
        }
        
        return schedules;
    }
    
    private BigDecimal calculateMonthlyPayment(BigDecimal principal, BigDecimal monthlyRate, int term) {
        // 等额本息计算公式
        BigDecimal temp = BigDecimal.ONE.add(monthlyRate).pow(term);
        return principal.multiply(monthlyRate)
                .multiply(temp)
                .divide(temp.subtract(BigDecimal.ONE), 2, BigDecimal.ROUND_HALF_UP);
    }
    
    private LoanAccountDTO convertToDTO(LoanAccount loanAccount) {
        LoanAccountDTO dto = new LoanAccountDTO();
        dto.setLoanAccountNo(loanAccount.getLoanAccountNo());
        dto.setApplicationNo(loanAccount.getApplicationNo());
        dto.setCustomerId(loanAccount.getCustomerId());
        dto.setAccountNumber(loanAccount.getAccountNumber());
        dto.setOriginalAmount(loanAccount.getOriginalAmount());
        dto.setOutstandingAmount(loanAccount.getOutstandingAmount());
        dto.setInterestAccrued(loanAccount.getInterestAccrued());
        dto.setInterestRate(loanAccount.getInterestRate());
        dto.setStartDate(loanAccount.getStartDate());
        dto.setMaturityDate(loanAccount.getMaturityDate());
        dto.setStatus(loanAccount.getStatus());
        return dto;
    }
    
    private RepaymentDTO convertToDTO(RepaymentSchedule schedule) {
        RepaymentDTO dto = new RepaymentDTO();
        dto.setId(schedule.getId());
        dto.setLoanAccountNo(schedule.getLoanAccountNo());
        dto.setInstallmentNo(schedule.getInstallmentNo());
        dto.setDueDate(schedule.getDueDate());
        dto.setPrincipalAmount(schedule.getPrincipalAmount());
        dto.setInterestAmount(schedule.getInterestAmount());
        dto.setTotalAmount(schedule.getTotalAmount());
        dto.setOutstandingPrincipal(schedule.getOutstandingPrincipal());
        dto.setStatus(schedule.getStatus());
        dto.setPaidDate(schedule.getPaidDate());
        dto.setPaidAmount(schedule.getPaidAmount());
        return dto;
    }
}

Repository层

public interface LoanApplicationRepository extends JpaRepository<LoanApplication, Long> {
    Optional<LoanApplication> findByApplicationNo(String applicationNo);
    
    List<LoanApplication> findByCustomerId(Long customerId);
    
    List<LoanApplication> findByStatus(LoanStatus status);
    
    @Query("SELECT la FROM LoanApplication la WHERE la.status = :status AND la.disbursementDate <= :date")
    List<LoanApplication> findPendingDisbursement(@Param("status") LoanStatus status, 
                                                @Param("date") LocalDate date);
}
public interface LoanAccountRepository extends JpaRepository<LoanAccount, Long> {
    Optional<LoanAccount> findByLoanAccountNo(String loanAccountNo);
    
    List<LoanAccount> findByCustomerId(Long customerId);
    
    List<LoanAccount> findByStatus(LoanAccountStatus status);
    
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT la FROM LoanAccount la WHERE la.loanAccountNo = :loanAccountNo")
    Optional<LoanAccount> findByLoanAccountNoForUpdate(@Param("loanAccountNo") String loanAccountNo);
    
    @Query("SELECT la FROM LoanAccount la WHERE la.status IN :statuses AND la.maturityDate <= :date")
    List<LoanAccount> findDueLoans(@Param("statuses") List<LoanAccountStatus> statuses,
                                 @Param("date") LocalDate date);
}
public interface RepaymentScheduleRepository extends JpaRepository<RepaymentSchedule, Long> {
    List<RepaymentSchedule> findByLoanAccountNo(String loanAccountNo);
    
    List<RepaymentSchedule> findByLoanAccountNoAndStatus(String loanAccountNo, RepaymentStatus status);
    
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT rs FROM RepaymentSchedule rs WHERE rs.id = :id")
    Optional<RepaymentSchedule> findByIdForUpdate(@Param("id") Long id);
    
    @Query("SELECT rs FROM RepaymentSchedule rs WHERE rs.loanAccountNo = :loanAccountNo AND rs.dueDate <= :date AND rs.status IN :statuses")
    List<RepaymentSchedule> findDueInstallments(@Param("loanAccountNo") String loanAccountNo,
                                              @Param("date") LocalDate date,
                                              @Param("statuses") List<RepaymentStatus> statuses);
}
public interface LoanProductRepository extends JpaRepository<LoanProduct, Long> {
    Optional<LoanProduct> findByProductCode(String productCode);
    
    List<LoanProduct> findByLoanType(LoanType loanType);
    
    List<LoanProduct> findByActiveTrue();
}

5. 客户关系管理(CRM)

客户信息管理

客户分级

客户行为分析

客户服务记录

# 在原有配置基础上添加
crm:
  customer:
    tier-review-interval: 180 # 客户等级评审间隔天数
  encryption:
    enabled: true
@Entity
@Table(name = "customer")
@Data
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String customerId;
    
    @Column(nullable = false)
    private String firstName;
    
    @Column(nullable = false)
    private String lastName;
    
    @Column(nullable = false, unique = true)
    private String idNumber;
    
    @Column(nullable = false)
    private LocalDate dateOfBirth;
    
    @Column(nullable = false)
    private String email;
    
    @Column(nullable = false)
    private String phone;
    
    @Column(nullable = false)
    private String address;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private CustomerTier tier;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private CustomerStatus status;
    
    @Column(nullable = false, precision = 19, scale = 4)
    private BigDecimal creditScore;
    
    @Column
    private LocalDate lastReviewDate;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
    
    @Version
    private Long version;
}
@Entity
@Table(name = "customer_interaction")
@Data
public class CustomerInteraction {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private Long customerId;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private InteractionType type;
    
    @Column(nullable = false)
    private String description;
    
    @Column
    private Long relatedAccountId;
    
    @Column
    private String notes;
    
    @Column(nullable = false)
    private Long employeeId;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
}
@Entity
@Table(name = "customer_preference")
@Data
public class CustomerPreference {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private Long customerId;
    
    @Column(nullable = false)
    private String preferenceType;
    
    @Column(nullable = false)
    private String preferenceValue;
    
    @Column
    private Boolean isActive = true;
}
@Entity
@Table(name = "customer_segment")
@Data
public class CustomerSegment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String segmentCode;
    
    @Column(nullable = false)
    private String segmentName;
    
    @Column
    private String description;
    
    @Column(nullable = false)
    private BigDecimal minValueScore;
    
    @Column(nullable = false)
    private BigDecimal maxValueScore;
    
    @Column(nullable = false)
    private Boolean isActive = true;
}
public enum CustomerTier {
    BASIC,      // 基础客户
    SILVER,     // 银牌客户
    GOLD,       // 金牌客户
    PLATINUM,   // 白金客户
    DIAMOND     // 钻石客户
}

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

public enum InteractionType {
    PHONE_CALL,     // 电话
    EMAIL,          // 邮件
    IN_PERSON,      // 面谈
    CHAT,           // 在线聊天
    COMPLAINT,      // 投诉
    COMPLIMENT,     // 表扬
    SERVICE_REQUEST // 服务请求
}
@Data
public class CustomerDTO {
    private Long id;
    private String customerId;
    private String firstName;
    private String lastName;
    private String idNumber;
    private LocalDate dateOfBirth;
    private String email;
    private String phone;
    private String address;
    private CustomerTier tier;
    private CustomerStatus status;
    private BigDecimal creditScore;
    private LocalDate lastReviewDate;
    private LocalDateTime createdAt;
}
@Data
public class CustomerCreateRequest {
    @NotBlank
    private String firstName;
    
    @NotBlank
    private String lastName;
    
    @NotBlank
    private String idNumber;
    
    @NotNull
    private LocalDate dateOfBirth;
    
    @Email
    @NotBlank
    private String email;
    
    @NotBlank
    private String phone;
    
    @NotBlank
    private String address;
}

@Data
public class CustomerUpdateRequest {
    @NotBlank
    private String firstName;
    
    @NotBlank
    private String lastName;
    
    @Email
    @NotBlank
    private String email;
    
    @NotBlank
    private String phone;
    
    @NotBlank
    private String address;
}
@Data
public class CustomerInteractionDTO {
    private Long id;
    private Long customerId;
    private InteractionType type;
    private String description;
    private Long relatedAccountId;
    private String notes;
    private Long employeeId;
    private LocalDateTime createdAt;
}
@Data
public class CustomerSegmentDTO {
    private Long id;
    private String segmentCode;
    private String segmentName;
    private String description;
    private BigDecimal minValueScore;
    private BigDecimal maxValueScore;
    private Boolean isActive;
}

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

工具

@Component
public class CustomerIdGenerator {
    private static final String BANK_CODE = "888";
    private final AtomicLong sequence = new AtomicLong(1);
    
    public String generate() {
        String dateStr = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
        long seq = sequence.getAndIncrement();
        return String.format("%s-CUS-%s-%06d", BANK_CODE, dateStr, seq);
    }
}

Controller层

@RestController
@RequestMapping("/api/customers")
@RequiredArgsConstructor
public class CustomerController {
    private final CustomerService customerService;
    
    @PostMapping
    public CustomerDTO createCustomer(@Valid @RequestBody CustomerCreateRequest request) {
        return customerService.createCustomer(request);
    }
    
    @PutMapping("/{customerId}")
    public CustomerDTO updateCustomer(
            @PathVariable String customerId,
            @Valid @RequestBody CustomerUpdateRequest request) {
        return customerService.updateCustomer(customerId, request);
    }
    
    @GetMapping("/{customerId}")
    public CustomerDTO getCustomer(@PathVariable String customerId) {
        return customerService.getCustomer(customerId);
    }
    
    @GetMapping("/tier/{tier}")
    public List<CustomerDTO> getCustomersByTier(@PathVariable String tier) {
        return customerService.getCustomersByTier(tier);
    }
    
    @PutMapping("/{customerId}/tier/{tier}")
    @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
    public CustomerDTO updateCustomerTier(
            @PathVariable String customerId,
            @PathVariable String tier) {
        return customerService.updateCustomerTier(customerId, tier);
    }
    
    @PutMapping("/{customerId}/status/{status}")
    @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
    public CustomerDTO updateCustomerStatus(
            @PathVariable String customerId,
            @PathVariable String status) {
        return customerService.updateCustomerStatus(customerId, status);
    }
    
    @GetMapping("/{customerId}/analysis")
    public CustomerAnalysisDTO getCustomerAnalysis(@PathVariable String customerId) {
        return customerService.getCustomerAnalysis(customerId);
    }
}
@RestController
@RequestMapping("/api/customer-interactions")
@RequiredArgsConstructor
public class CustomerInteractionController {
    private final CustomerInteractionService interactionService;
    
    @PostMapping
    public CustomerInteractionDTO recordInteraction(
            @RequestParam String customerId,
            @RequestParam InteractionType type,
            @RequestParam String description,
            @RequestParam(required = false) Long relatedAccountId,
            @RequestParam(required = false) String notes,
            @RequestParam Long employeeId) {
        
        return interactionService.recordInteraction(
                customerId, type, description, relatedAccountId, notes, employeeId);
    }
    
    @GetMapping("/customer/{customerId}")
    public List<CustomerInteractionDTO> getCustomerInteractions(@PathVariable String customerId) {
        return interactionService.getCustomerInteractions(customerId);
    }
    
    @GetMapping("/employee/{employeeId}")
    public List<CustomerInteractionDTO> getEmployeeInteractions(
            @PathVariable Long employeeId,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime end) {
        
        return interactionService.getEmployeeInteractions(employeeId, start, end);
    }
}

Service层

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomerService {
    private final CustomerRepository customerRepository;
    private final CustomerIdGenerator idGenerator;
    private final EncryptionUtil encryptionUtil;
    private final CustomerAnalysisService analysisService;
    
    @Transactional
    public CustomerDTO createCustomer(CustomerCreateRequest request) {
        // 检查身份证号是否已存在
        if (customerRepository.findByIdNumber(request.getIdNumber()).isPresent()) {
            throw new AccountException(ErrorCode.CUSTOMER_ALREADY_EXISTS);
        }
        
        // 创建客户
        Customer customer = new Customer();
        customer.setCustomerId(idGenerator.generate());
        customer.setFirstName(request.getFirstName());
        customer.setLastName(request.getLastName());
        customer.setIdNumber(encryptionUtil.encrypt(request.getIdNumber()));
        customer.setDateOfBirth(request.getDateOfBirth());
        customer.setEmail(request.getEmail());
        customer.setPhone(encryptionUtil.encrypt(request.getPhone()));
        customer.setAddress(request.getAddress());
        customer.setTier(CustomerTier.BASIC);
        customer.setStatus(CustomerStatus.ACTIVE);
        customer.setCreditScore(calculateInitialCreditScore());
        
        Customer saved = customerRepository.save(customer);
        log.info("Customer created: {}", saved.getCustomerId());
        
        // 初始化客户分析数据
        analysisService.initializeCustomerAnalysis(saved.getId());
        
        return convertToDTO(saved);
    }
    
    @Transactional
    public CustomerDTO updateCustomer(String customerId, CustomerUpdateRequest request) {
        Customer customer = customerRepository.findByCustomerId(customerId)
                .orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));
        
        customer.setFirstName(request.getFirstName());
        customer.setLastName(request.getLastName());
        customer.setEmail(request.getEmail());
        customer.setPhone(encryptionUtil.encrypt(request.getPhone()));
        customer.setAddress(request.getAddress());
        
        Customer saved = customerRepository.save(customer);
        log.info("Customer updated: {}", customerId);
        
        return convertToDTO(saved);
    }
    
    @Transactional(readOnly = true)
    public CustomerDTO getCustomer(String customerId) {
        Customer customer = customerRepository.findByCustomerId(customerId)
                .orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));
        
        CustomerDTO dto = convertToDTO(customer);
        dto.setPhone(encryptionUtil.decrypt(dto.getPhone()));
        return dto;
    }
    
    @Transactional(readOnly = true)
    public List<CustomerDTO> getCustomersByTier(String tier) {
        return customerRepository.findByTier(CustomerTier.valueOf(tier))
                .stream()
                .map(this::convertToDTO)
                .peek(dto -> dto.setPhone(encryptionUtil.decrypt(dto.getPhone())))
                .collect(Collectors.toList());
    }
    
    @Transactional
    @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
    public CustomerDTO updateCustomerTier(String customerId, String tier) {
        Customer customer = customerRepository.findByCustomerId(customerId)
                .orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));
        
        customer.setTier(CustomerTier.valueOf(tier));
        customer.setLastReviewDate(LocalDate.now());
        
        Customer saved = customerRepository.save(customer);
        log.info("Customer tier updated to {}: {}", tier, customerId);
        
        return convertToDTO(saved);
    }
    
    @Transactional
    @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
    public CustomerDTO updateCustomerStatus(String customerId, String status) {
        Customer customer = customerRepository.findByCustomerId(customerId)
                .orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));
        
        customer.setStatus(CustomerStatus.valueOf(status));
        
        Customer saved = customerRepository.save(customer);
        log.info("Customer status updated to {}: {}", status, customerId);
        
        return convertToDTO(saved);
    }
    
    @Transactional(readOnly = true)
    public CustomerAnalysisDTO getCustomerAnalysis(String customerId) {
        Customer customer = customerRepository.findByCustomerId(customerId)
                .orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));
        
        return analysisService.getCustomerAnalysis(customer.getId());
    }
    
    @Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行
    public void reviewCustomerTiers() {
        LocalDate reviewDate = LocalDate.now().minusMonths(6);
        List<Customer> customers = customerRepository.findCustomersNeedingReview(reviewDate);
        
        customers.forEach(customer -> {
            try {
                reviewCustomerTier(customer);
            } catch (Exception e) {
                log.error("Failed to review customer tier: " + customer.getCustomerId(), e);
            }
        });
    }
    
    private void reviewCustomerTier(Customer customer) {
        CustomerAnalysisDTO analysis = analysisService.getCustomerAnalysis(customer.getId());
        
        CustomerTier newTier = determineCustomerTier(analysis);
        if (newTier != customer.getTier()) {
            customer.setTier(newTier);
            customer.setLastReviewDate(LocalDate.now());
            customerRepository.save(customer);
            log.info("Customer tier updated to {}: {}", newTier, customer.getCustomerId());
        }
    }
    
    private CustomerTier determineCustomerTier(CustomerAnalysisDTO analysis) {
        BigDecimal score = analysis.getProfitabilityScore();
        
        if (score.compareTo(BigDecimal.valueOf(90)) >= 0) {
            return CustomerTier.DIAMOND;
        } else if (score.compareTo(BigDecimal.valueOf(75)) >= 0) {
            return CustomerTier.PLATINUM;
        } else if (score.compareTo(BigDecimal.valueOf(60)) >= 0) {
            return CustomerTier.GOLD;
        } else if (score.compareTo(BigDecimal.valueOf(40)) >= 0) {
            return CustomerTier.SILVER;
        } else {
            return CustomerTier.BASIC;
        }
    }
    
    private BigDecimal calculateInitialCreditScore() {
        // 简化处理,实际业务中可能有更复杂的信用评分计算逻辑
        return BigDecimal.valueOf(65); // 初始信用分65
    }
    
    private CustomerDTO convertToDTO(Customer customer) {
        CustomerDTO dto = new CustomerDTO();
        dto.setId(customer.getId());
        dto.setCustomerId(customer.getCustomerId());
        dto.setFirstName(customer.getFirstName());
        dto.setLastName(customer.getLastName());
        dto.setIdNumber(customer.getIdNumber());
        dto.setDateOfBirth(customer.getDateOfBirth());
        dto.setEmail(customer.getEmail());
        dto.setPhone(customer.getPhone());
        dto.setAddress(customer.getAddress());
        dto.setTier(customer.getTier());
        dto.setStatus(customer.getStatus());
        dto.setCreditScore(customer.getCreditScore());
        dto.setLastReviewDate(customer.getLastReviewDate());
        dto.setCreatedAt(customer.getCreatedAt());
        return dto;
    }
}
@Service
@RequiredArgsConstructor
@Slf4j
public class CustomerAnalysisService {
    private final CustomerRepository customerRepository;
    private final AccountClientService accountClientService;
    private final LoanClientService loanClientService;
    private final TransactionClientService transactionClientService;
    
    @Transactional
    public void initializeCustomerAnalysis(Long customerId) {
        // 在实际项目中,这里会初始化客户的分析数据
        log.info("Initialized analysis data for customer: {}", customerId);
    }
    
    @Transactional(readOnly = true)
    public CustomerAnalysisDTO getCustomerAnalysis(Long customerId) {
        Customer customer = customerRepository.findById(customerId)
                .orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));
        
        CustomerAnalysisDTO analysis = new CustomerAnalysisDTO();
        analysis.setCustomerId(customer.getId());
        analysis.setCustomerName(customer.getFirstName() + " " + customer.getLastName());
        
        // 获取客户资产数据
        BigDecimal totalAssets = accountClientService.getCustomerTotalAssets(customer.getCustomerId());
        analysis.setTotalAssets(totalAssets);
        
        // 获取客户负债数据
        BigDecimal totalLiabilities = loanClientService.getCustomerTotalLiabilities(customer.getCustomerId());
        analysis.setTotalLiabilities(totalLiabilities);
        
        // 计算净资产
        analysis.setNetWorth(totalAssets.subtract(totalLiabilities));
        
        // 获取月度收支数据
        analysis.setMonthlyIncome(transactionClientService.getMonthlyIncome(customer.getCustomerId()));
        analysis.setMonthlyExpenses(transactionClientService.getMonthlyExpenses(customer.getCustomerId()));
        
        // 计算盈利性评分
        analysis.setProfitabilityScore(calculateProfitabilityScore(analysis));
        
        // 获取最后活动日期
        analysis.setLastActivityDate(transactionClientService.getLastActivityDate(customer.getCustomerId()));
        
        // 获取产品数量
        analysis.setProductCount(
                accountClientService.getAccountCount(customer.getCustomerId()) +
                loanClientService.getLoanCount(customer.getCustomerId()));
        
        return analysis;
    }
    
    private BigDecimal calculateProfitabilityScore(CustomerAnalysisDTO analysis) {
        // 简化处理,实际业务中可能有更复杂的评分逻辑
        BigDecimal score = BigDecimal.ZERO;
        
        // 净资产贡献
        if (analysis.getNetWorth().compareTo(BigDecimal.valueOf(1000000)) >= 0) {
            score = score.add(BigDecimal.valueOf(40));
        } else if (analysis.getNetWorth().compareTo(BigDecimal.valueOf(500000)) >= 0) {
            score = score.add(BigDecimal.valueOf(30));
        } else if (analysis.getNetWorth().compareTo(BigDecimal.valueOf(100000)) >= 0) {
            score = score.add(BigDecimal.valueOf(20));
        } else {
            score = score.add(BigDecimal.valueOf(10));
        }
        
        // 月收入贡献
        if (analysis.getMonthlyIncome().compareTo(BigDecimal.valueOf(50000)) >= 0) {
            score = score.add(BigDecimal.valueOf(30));
        } else if (analysis.getMonthlyIncome().compareTo(BigDecimal.valueOf(20000)) >= 0) {
            score = score.add(BigDecimal.valueOf(20));
        } else if (analysis.getMonthlyIncome().compareTo(BigDecimal.valueOf(5000)) >= 0) {
            score = score.add(BigDecimal.valueOf(15));
        } else {
            score = score.add(BigDecimal.valueOf(5));
        }
        
        // 产品数量贡献
        score = score.add(BigDecimal.valueOf(Math.min(analysis.getProductCount(), 10) * 2));
        
        // 活动频率贡献
        long daysSinceLastActivity = LocalDate.now().toEpochDay() - 
                analysis.getLastActivityDate().toEpochDay();
        if (daysSinceLastActivity <= 7) {
            score = score.add(BigDecimal.valueOf(20));
        } else if (daysSinceLastActivity <= 30) {
            score = score.add(BigDecimal.valueOf(10));
        }
        
        return score.setScale(0, RoundingMode.HALF_UP);
    }
}
@Service
@RequiredArgsConstructor
@Slf4j
public class CustomerInteractionService {
    private final CustomerInteractionRepository interactionRepository;
    private final CustomerRepository customerRepository;
    
    @Transactional
    public CustomerInteractionDTO recordInteraction(
            String customerId, 
            InteractionType type, 
            String description, 
            Long relatedAccountId, 
            String notes,
            Long employeeId) {
        
        Customer customer = customerRepository.findByCustomerId(customerId)
                .orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));
        
        CustomerInteraction interaction = new CustomerInteraction();
        interaction.setCustomerId(customer.getId());
        interaction.setType(type);
        interaction.setDescription(description);
        interaction.setRelatedAccountId(relatedAccountId);
        interaction.setNotes(notes);
        interaction.setEmployeeId(employeeId);
        
        CustomerInteraction saved = interactionRepository.save(interaction);
        log.info("Interaction recorded for customer: {}", customerId);
        
        return convertToDTO(saved);
    }
    
    @Transactional(readOnly = true)
    public List<CustomerInteractionDTO> getCustomerInteractions(String customerId) {
        Customer customer = customerRepository.findByCustomerId(customerId)
                .orElseThrow(() -> new AccountException(ErrorCode.CUSTOMER_NOT_FOUND));
        
        return interactionRepository.findByCustomerId(customer.getId())
                .stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }
    
    @Transactional(readOnly = true)
    public List<CustomerInteractionDTO> getEmployeeInteractions(
            Long employeeId, 
            LocalDateTime start, 
            LocalDateTime end) {
        
        return interactionRepository.findByEmployeeIdAndCreatedAtBetween(employeeId, start, end)
                .stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }
    
    private CustomerInteractionDTO convertToDTO(CustomerInteraction interaction) {
        CustomerInteractionDTO dto = new CustomerInteractionDTO();
        dto.setId(interaction.getId());
        dto.setCustomerId(interaction.getCustomerId());
        dto.setType(interaction.getType());
        dto.setDescription(interaction.getDescription());
        dto.setRelatedAccountId(interaction.getRelatedAccountId());
        dto.setNotes(interaction.getNotes());
        dto.setEmployeeId(interaction.getEmployeeId());
        dto.setCreatedAt(interaction.getCreatedAt());
        return dto;
    }
}

Repository层

public interface CustomerRepository extends JpaRepository<Customer, Long> {
    Optional<Customer> findByCustomerId(String customerId);
    
    Optional<Customer> findByIdNumber(String idNumber);
    
    List<Customer> findByTier(CustomerTier tier);
    
    List<Customer> findByStatus(CustomerStatus status);
    
    List<Customer> findByCreditScoreBetween(BigDecimal minScore, BigDecimal maxScore);
    
    @Query("SELECT c FROM Customer c WHERE c.lastReviewDate IS NULL OR c.lastReviewDate < :date")
    List<Customer> findCustomersNeedingReview(@Param("date") LocalDate date);
}
public interface CustomerInteractionRepository extends JpaRepository<CustomerInteraction, Long> {
    List<CustomerInteraction> findByCustomerId(Long customerId);
    
    List<CustomerInteraction> findByCustomerIdAndType(Long customerId, InteractionType type);
    
    List<CustomerInteraction> findByEmployeeIdAndCreatedAtBetween(
            Long employeeId, LocalDateTime start, LocalDateTime end);
}
public interface CustomerPreferenceRepository extends JpaRepository<CustomerPreference, Long> {
    List<CustomerPreference> findByCustomerId(Long customerId);
    
    Optional<CustomerPreference> findByCustomerIdAndPreferenceType(
            Long customerId, String preferenceType);
    
    List<CustomerPreference> findByPreferenceTypeAndPreferenceValue(
            String preferenceType, String preferenceValue);
}
public interface CustomerSegmentRepository extends JpaRepository<CustomerSegment, Long> {
    Optional<CustomerSegment> findBySegmentCode(String segmentCode);
    
    List<CustomerSegment> findByIsActiveTrue();
}

6. 风险管理

反欺诈检测

交易监控

合规检查

风险预警

# 在原有配置基础上添加
risk:
  engine:
    rules-refresh-interval: 300000 # 规则刷新间隔(5分钟)
  blacklist:
    cache-enabled: true
    cache-ttl: 3600000 # 1小时
@Entity
@Table(name = "risk_rule")
@Data
public class RiskRule {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String ruleCode;
    
    @Column(nullable = false)
    private String ruleName;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private RiskRuleCategory category;
    
    @Column(nullable = false, columnDefinition = "TEXT")
    private String ruleExpression;
    
    @Column(nullable = false)
    private Integer priority;
    
    @Column(nullable = false)
    private Boolean isActive = true;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private RiskRuleStatus status = RiskRuleStatus.DRAFT;
    
    @Column
    private String action;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

@Entity
@Table(name = "risk_event")
@Data
public class RiskEvent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String eventId;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private RiskEventType eventType;
    
    @Column(nullable = false)
    private String ruleCode;
    
    @Column
    private Long customerId;
    
    @Column
    private String accountNumber;
    
    @Column
    private String transactionId;
    
    @Column(precision = 19, scale = 4)
    private BigDecimal amount;
    
    @Column(nullable = false)
    private String description;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private RiskEventStatus status = RiskEventStatus.PENDING;
    
    @Column
    private Long handledBy;
    
    @Column
    private LocalDateTime handledAt;
    
    @Column
    private String handlingNotes;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}
@Entity
@Table(name = "risk_parameter")
@Data
public class RiskParameter {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String paramKey;
    
    @Column(nullable = false)
    private String paramName;
    
    @Column(nullable = false)
    private String paramValue;
    
    @Column
    private String description;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

@Entity
@Table(name = "risk_blacklist")
@Data
public class RiskBlacklist {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String entityType; // CUSTOMER, ACCOUNT, IP, DEVICE, etc.
    
    @Column(nullable = false)
    private String entityValue;
    
    @Column(nullable = false)
    private String reason;
    
    @Column(nullable = false)
    private String source;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
}
public enum RiskRuleCategory {
    FRAUD_DETECTION,      // 欺诈检测
    AML_COMPLIANCE,       // 反洗钱合规
    TRANSACTION_MONITORING, // 交易监控
    CREDIT_RISK,          // 信用风险
    OPERATIONAL_RISK      // 操作风险
}
public enum RiskRuleStatus {
    DRAFT,        // 草稿
    TESTING,      // 测试中
    PRODUCTION,   // 生产中
    DISABLED      // 已禁用
}
public enum RiskEventType {
    SUSPICIOUS_TRANSACTION, // 可疑交易
    HIGH_RISK_CUSTOMER,     // 高风险客户
    AML_ALERT,              // 反洗钱警报
    FRAUD_ATTEMPT,          // 欺诈尝试
    POLICY_VIOLATION        // 政策违规
}
public enum RiskEventStatus {
    PENDING,      // 待处理
    REVIEWING,    // 审核中
    APPROVED,     // 已批准
    REJECTED,     // 已拒绝
    MITIGATED     // 已缓解
}
@Data
public class RiskRuleDTO {
    private Long id;
    private String ruleCode;
    private String ruleName;
    private RiskRuleCategory category;
    private String ruleExpression;
    private Integer priority;
    private Boolean isActive;
    private RiskRuleStatus status;
    private String action;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
@Data
public class RiskRuleRequest {
    @NotBlank
    private String ruleName;
    
    @NotNull
    private RiskRuleCategory category;
    
    @NotBlank
    private String ruleExpression;
    
    @NotNull
    @Min(1)
    @Max(100)
    private Integer priority;
    
    private String action;
}
@Data
public class RiskEventDTO {
    private Long id;
    private String eventId;
    private RiskEventType eventType;
    private String ruleCode;
    private Long customerId;
    private String accountNumber;
    private String transactionId;
    private BigDecimal amount;
    private String description;
    private RiskEventStatus status;
    private Long handledBy;
    private LocalDateTime handledAt;
    private String handlingNotes;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
@Data
public class RiskParameterDTO {
    private Long id;
    private String paramKey;
    private String paramName;
    private String paramValue;
    private String description;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

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

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

规则引擎工具

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

Controller层

@RestController
@RequestMapping("/api/risk/rules")
@RequiredArgsConstructor
public class RiskRuleController {
    private final RiskRuleService ruleService;
    
    @PostMapping
    @PreAuthorize("hasAnyRole('RISK_MANAGER', 'ADMIN')")
    public RiskRuleDTO createRule(@Valid @RequestBody RiskRuleRequest request) {
        return ruleService.createRule(request);
    }
    
    @PutMapping("/{ruleCode}/status/{status}")
    @PreAuthorize("hasAnyRole('RISK_MANAGER', 'ADMIN')")
    public RiskRuleDTO updateRuleStatus(
            @PathVariable String ruleCode,
            @PathVariable RiskRuleStatus status) {
        return ruleService.updateRuleStatus(ruleCode, status);
    }
    
    @PutMapping("/{ruleCode}/active/{isActive}")
    @PreAuthorize("hasAnyRole('RISK_MANAGER', 'ADMIN')")
    public RiskRuleDTO toggleRuleActivation(
            @PathVariable String ruleCode,
            @PathVariable Boolean isActive) {
        return ruleService.toggleRuleActivation(ruleCode, isActive);
    }
    
    @GetMapping("/category/{category}")
    public List<RiskRuleDTO> getRulesByCategory(@PathVariable String category) {
        return ruleService.getActiveRulesByCategory(RiskRuleCategory.valueOf(category));
    }
    
    @GetMapping
    public List<RiskRuleDTO> getAllActiveRules() {
        return ruleService.getAllActiveRules();
    }
}
@RestController
@RequestMapping("/api/risk/assessments")
@RequiredArgsConstructor
public class RiskAssessmentController {
    private final RiskEngineService riskEngineService;
    
    @PostMapping("/transactions")
    public RiskAssessmentResult assessTransactionRisk(
            @RequestParam String transactionId,
            @RequestParam String accountNumber,
            @RequestParam Long customerId,
            @RequestParam BigDecimal amount,
            @RequestBody Map<String, Object> transactionData) {
        
        return riskEngineService.assessTransactionRisk(
                transactionId, accountNumber, customerId, amount, transactionData);
    }
    
    @PostMapping("/customers/{customerId}")
    public RiskAssessmentResult assessCustomerRisk(
            @PathVariable Long customerId,
            @RequestBody Map<String, Object> customerData) {
        
        return riskEngineService.assessCustomerRisk(customerId, customerData);
    }
}
@RestController
@RequestMapping("/api/risk/events")
@RequiredArgsConstructor
public class RiskEventController {
    private final RiskEventService eventService;
    
    @GetMapping("/pending")
    @PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
    public List<RiskEventDTO> getPendingEvents() {
        return eventService.getPendingEvents();
    }
    
    @GetMapping("/customers/{customerId}")
    @PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
    public List<RiskEventDTO> getCustomerEvents(
            @PathVariable Long customerId,
            @RequestParam(defaultValue = "30") int days) {
        return eventService.getEventsByCustomer(customerId, days);
    }
    
    @PutMapping("/{eventId}/status/{status}")
    @PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
    public RiskEventDTO updateEventStatus(
            @PathVariable String eventId,
            @PathVariable RiskEventStatus status,
            @RequestParam String notes,
            @RequestParam Long userId) {
        return eventService.updateEventStatus(eventId, status, notes, userId);
    }
}

@RestController
@RequestMapping("/api/risk/blacklist")
@RequiredArgsConstructor
public class RiskBlacklistController {
    private final RiskBlacklistService blacklistService;
    
    @PostMapping
    @PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
    public RiskBlacklistDTO addToBlacklist(
            @RequestParam String entityType,
            @RequestParam String entityValue,
            @RequestParam String reason,
            @RequestParam String source) {
        return blacklistService.addToBlacklist(entityType, entityValue, reason, source);
    }
    
    @DeleteMapping("/{id}")
    @PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
    public void removeFromBlacklist(@PathVariable Long id) {
        blacklistService.removeFromBlacklist(id);
    }
    
    @GetMapping("/check")
    public boolean isBlacklisted(
            @RequestParam String entityType,
            @RequestParam String entityValue) {
        return blacklistService.isBlacklisted(entityType, entityValue);
    }
    
    @GetMapping("/type/{entityType}")
    @PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
    public List<RiskBlacklistDTO> getBlacklistByType(@PathVariable String entityType) {
        return blacklistService.getBlacklistByType(entityType);
    }
}

Service层

@Service
@RequiredArgsConstructor
@Slf4j
public class RiskRuleService {
    private final RiskRuleRepository ruleRepository;
    private final RuleCodeGenerator codeGenerator;
    
    @Transactional
    @PreAuthorize("hasAnyRole('RISK_MANAGER', 'ADMIN')")
    public RiskRuleDTO createRule(RiskRuleRequest request) {
        RiskRule rule = new RiskRule();
        rule.setRuleCode(codeGenerator.generate());
        rule.setRuleName(request.getRuleName());
        rule.setCategory(request.getCategory());
        rule.setRuleExpression(request.getRuleExpression());
        rule.setPriority(request.getPriority());
        rule.setAction(request.getAction());
        
        RiskRule saved = ruleRepository.save(rule);
        log.info("Risk rule created: {}", saved.getRuleCode());
        
        return convertToDTO(saved);
    }
    
    @Transactional
    @PreAuthorize("hasAnyRole('RISK_MANAGER', 'ADMIN')")
    public RiskRuleDTO updateRuleStatus(String ruleCode, RiskRuleStatus status) {
        RiskRule rule = ruleRepository.findByRuleCode(ruleCode)
                .orElseThrow(() -> new AccountException(ErrorCode.RISK_RULE_NOT_FOUND));
        
        rule.setStatus(status);
        RiskRule saved = ruleRepository.save(rule);
        log.info("Risk rule status updated to {}: {}", status, ruleCode);
        
        return convertToDTO(saved);
    }
    
    @Transactional
    @PreAuthorize("hasAnyRole('RISK_MANAGER', 'ADMIN')")
    public RiskRuleDTO toggleRuleActivation(String ruleCode, Boolean isActive) {
        RiskRule rule = ruleRepository.findByRuleCode(ruleCode)
                .orElseThrow(() -> new AccountException(ErrorCode.RISK_RULE_NOT_FOUND));
        
        rule.setIsActive(isActive);
        RiskRule saved = ruleRepository.save(rule);
        log.info("Risk rule activation set to {}: {}", isActive, ruleCode);
        
        return convertToDTO(saved);
    }
    
    @Transactional(readOnly = true)
    public List<RiskRuleDTO> getActiveRulesByCategory(RiskRuleCategory category) {
        return ruleRepository.findByCategoryAndIsActiveTrue(category)
                .stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }
    
    @Transactional(readOnly = true)
    public List<RiskRuleDTO> getAllActiveRules() {
        return ruleRepository.findAllActiveRules()
                .stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }
    
    private RiskRuleDTO convertToDTO(RiskRule rule) {
        RiskRuleDTO dto = new RiskRuleDTO();
        dto.setId(rule.getId());
        dto.setRuleCode(rule.getRuleCode());
        dto.setRuleName(rule.getRuleName());
        dto.setCategory(rule.getCategory());
        dto.setRuleExpression(rule.getRuleExpression());
        dto.setPriority(rule.getPriority());
        dto.setIsActive(rule.getIsActive());
        dto.setStatus(rule.getStatus());
        dto.setAction(rule.getAction());
        dto.setCreatedAt(rule.getCreatedAt());
        dto.setUpdatedAt(rule.getUpdatedAt());
        return dto;
    }
}
/**
	风险管理引擎
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class RiskEngineService {
    private final RiskRuleRepository ruleRepository;
    private final RiskEventService eventService;
    private final RiskEngineHelper engineHelper;
    
    @Transactional
    public RiskAssessmentResult assessTransactionRisk(
            String transactionId,
            String accountNumber,
            Long customerId,
            BigDecimal amount,
            Map<String, Object> transactionData) {
        
        List<RiskRule> activeRules = ruleRepository.findAllActiveRules();
        BigDecimal riskScore = BigDecimal.ZERO;
        Map<String, String> triggeredRules = new HashMap<>();
        
        for (RiskRule rule : activeRules) {
            try {
                boolean isTriggered = engineHelper.evaluateRule(rule.getRuleExpression(), transactionData);
                if (isTriggered) {
                    riskScore = riskScore.add(BigDecimal.valueOf(rule.getPriority()));
                    triggeredRules.put(rule.getRuleCode(), rule.getRuleName());
                    
                    // 记录风险事件
                    RiskEventDTO event = new RiskEventDTO();
                    event.setEventType(RiskEventType.SUSPICIOUS_TRANSACTION);
                    event.setRuleCode(rule.getRuleCode());
                    event.setCustomerId(customerId);
                    event.setAccountNumber(accountNumber);
                    event.setTransactionId(transactionId);
                    event.setAmount(amount);
                    event.setDescription(rule.getRuleName() + " triggered");
                    event.setStatus(RiskEventStatus.PENDING);
                    
                    eventService.createEvent(event);
                }
            } catch (Exception e) {
                log.error("Error evaluating risk rule {}: {}", rule.getRuleCode(), e.getMessage());
            }
        }
        
        RiskAssessmentResult result = new RiskAssessmentResult();
        result.setTransactionId(transactionId);
        result.setRiskScore(riskScore);
        result.setRiskLevel(determineRiskLevel(riskScore));
        result.setTriggeredRules(triggeredRules);
        result.setRecommendation(generateRecommendation(riskScore));
        
        log.info("Risk assessment completed for transaction {}: score {}", transactionId, riskScore);
        
        return result;
    }
    
    @Transactional
    public RiskAssessmentResult assessCustomerRisk(Long customerId, Map<String, Object> customerData) {
        List<RiskRule> activeRules = ruleRepository.findAllActiveRules();
        BigDecimal riskScore = BigDecimal.ZERO;
        Map<String, String> triggeredRules = new HashMap<>();
        
        for (RiskRule rule : activeRules) {
            try {
                boolean isTriggered = engineHelper.evaluateRule(rule.getRuleExpression(), customerData);
                if (isTriggered) {
                    riskScore = riskScore.add(BigDecimal.valueOf(rule.getPriority()));
                    triggeredRules.put(rule.getRuleCode(), rule.getRuleName());
                    
                    // 记录风险事件
                    RiskEventDTO event = new RiskEventDTO();
                    event.setEventType(RiskEventType.HIGH_RISK_CUSTOMER);
                    event.setRuleCode(rule.getRuleCode());
                    event.setCustomerId(customerId);
                    event.setDescription(rule.getRuleName() + " triggered");
                    event.setStatus(RiskEventStatus.PENDING);
                    
                    eventService.createEvent(event);
                }
            } catch (Exception e) {
                log.error("Error evaluating risk rule {}: {}", rule.getRuleCode(), e.getMessage());
            }
        }
        
        RiskAssessmentResult result = new RiskAssessmentResult();
        result.setRiskScore(riskScore);
        result.setRiskLevel(determineRiskLevel(riskScore));
        result.setTriggeredRules(triggeredRules);
        result.setRecommendation(generateRecommendation(riskScore));
        
        log.info("Risk assessment completed for customer {}: score {}", customerId, riskScore);
        
        return result;
    }
    
    private String determineRiskLevel(BigDecimal score) {
        if (score.compareTo(BigDecimal.valueOf(80)) >= 0) {
            return "CRITICAL";
        } else if (score.compareTo(BigDecimal.valueOf(50)) >= 0) {
            return "HIGH";
        } else if (score.compareTo(BigDecimal.valueOf(30)) >= 0) {
            return "MEDIUM";
        } else if (score.compareTo(BigDecimal.valueOf(10)) >= 0) {
            return "LOW";
        } else {
            return "NORMAL";
        }
    }
    
    private String generateRecommendation(BigDecimal score) {
        if (score.compareTo(BigDecimal.valueOf(80)) >= 0) {
            return "BLOCK and ALERT";
        } else if (score.compareTo(BigDecimal.valueOf(50)) >= 0) {
            return "REVIEW and VERIFY";
        } else if (score.compareTo(BigDecimal.valueOf(30)) >= 0) {
            return "MONITOR CLOSELY";
        } else if (score.compareTo(BigDecimal.valueOf(10)) >= 0) {
            return "STANDARD MONITORING";
        } else {
            return "NO ACTION REQUIRED";
        }
    }
}
/**
	风险事件服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class RiskEventService {
    private final RiskEventRepository eventRepository;
    private final EventIdGenerator idGenerator;
    
    @Transactional
    public RiskEventDTO createEvent(RiskEventDTO eventDTO) {
        RiskEvent event = new RiskEvent();
        event.setEventId(idGenerator.generate());
        event.setEventType(eventDTO.getEventType());
        event.setRuleCode(eventDTO.getRuleCode());
        event.setCustomerId(eventDTO.getCustomerId());
        event.setAccountNumber(eventDTO.getAccountNumber());
        event.setTransactionId(eventDTO.getTransactionId());
        event.setAmount(eventDTO.getAmount());
        event.setDescription(eventDTO.getDescription());
        event.setStatus(eventDTO.getStatus());
        
        RiskEvent saved = eventRepository.save(event);
        log.info("Risk event created: {}", saved.getEventId());
        
        return convertToDTO(saved);
    }
    
    @Transactional
    @PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
    public RiskEventDTO updateEventStatus(String eventId, RiskEventStatus status, String notes, Long userId) {
        RiskEvent event = eventRepository.findByEventId(eventId)
                .orElseThrow(() -> new AccountException(ErrorCode.RISK_EVENT_NOT_FOUND));
        
        event.setStatus(status);
        event.setHandledBy(userId);
        event.setHandledAt(LocalDateTime.now());
        event.setHandlingNotes(notes);
        
        RiskEvent saved = eventRepository.save(event);
        log.info("Risk event status updated to {}: {}", status, eventId);
        
        return convertToDTO(saved);
    }
    
    @Transactional(readOnly = true)
    public List<RiskEventDTO> getPendingEvents() {
        return eventRepository.findPendingEvents()
                .stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }
    
    @Transactional(readOnly = true)
    public List<RiskEventDTO> getEventsByCustomer(Long customerId, int days) {
        LocalDateTime date = LocalDateTime.now().minusDays(days);
        return eventRepository.findByCustomerIdAndCreatedAtAfter(customerId, date)
                .stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }
    
    private RiskEventDTO convertToDTO(RiskEvent event) {
        RiskEventDTO dto = new RiskEventDTO();
        dto.setId(event.getId());
        dto.setEventId(event.getEventId());
        dto.setEventType(event.getEventType());
        dto.setRuleCode(event.getRuleCode());
        dto.setCustomerId(event.getCustomerId());
        dto.setAccountNumber(event.getAccountNumber());
        dto.setTransactionId(event.getTransactionId());
        dto.setAmount(event.getAmount());
        dto.setDescription(event.getDescription());
        dto.setStatus(event.getStatus());
        dto.setHandledBy(event.getHandledBy());
        dto.setHandledAt(event.getHandledAt());
        dto.setHandlingNotes(event.getHandlingNotes());
        dto.setCreatedAt(event.getCreatedAt());
        dto.setUpdatedAt(event.getUpdatedAt());
        return dto;
    }
}
/**
	黑名单服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class RiskBlacklistService {
    private final RiskBlacklistRepository blacklistRepository;
    
    @Transactional
    @PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
    public RiskBlacklistDTO addToBlacklist(String entityType, String entityValue, String reason, String source) {
        if (blacklistRepository.findByEntityTypeAndEntityValue(entityType, entityValue).isPresent()) {
            throw new AccountException(ErrorCode.ENTITY_ALREADY_BLACKLISTED);
        }
        
        RiskBlacklist entry = new RiskBlacklist();
        entry.setEntityType(entityType);
        entry.setEntityValue(entityValue);
        entry.setReason(reason);
        entry.setSource(source);
        
        RiskBlacklist saved = blacklistRepository.save(entry);
        log.info("Added to blacklist: {} - {}", entityType, entityValue);
        
        return convertToDTO(saved);
    }
    
    @Transactional
    @PreAuthorize("hasAnyRole('RISK_OFFICER', 'RISK_MANAGER', 'ADMIN')")
    public void removeFromBlacklist(Long id) {
        RiskBlacklist entry = blacklistRepository.findById(id)
                .orElseThrow(() -> new AccountException(ErrorCode.BLACKLIST_ENTRY_NOT_FOUND));
        
        blacklistRepository.delete(entry);
        log.info("Removed from blacklist: {} - {}", entry.getEntityType(), entry.getEntityValue());
    }
    
    @Transactional(readOnly = true)
    public boolean isBlacklisted(String entityType, String entityValue) {
        return blacklistRepository.findByEntityTypeAndEntityValue(entityType, entityValue).isPresent();
    }
    
    @Transactional(readOnly = true)
    public List<RiskBlacklistDTO> getBlacklistByType(String entityType) {
        return blacklistRepository.findByEntityType(entityType)
                .stream()
                .map(this::convertToDTO)
                .collect(Collectors.toList());
    }
    
    private RiskBlacklistDTO convertToDTO(RiskBlacklist entry) {
        RiskBlacklistDTO dto = new RiskBlacklistDTO();
        dto.setId(entry.getId());
        dto.setEntityType(entry.getEntityType());
        dto.setEntityValue(entry.getEntityValue());
        dto.setReason(entry.getReason());
        dto.setSource(entry.getSource());
        dto.setCreatedAt(entry.getCreatedAt());
        return dto;
    }
}

Repository层

public interface RiskRuleRepository extends JpaRepository<RiskRule, Long> {
    Optional<RiskRule> findByRuleCode(String ruleCode);
    
    List<RiskRule> findByCategoryAndIsActiveTrue(RiskRuleCategory category);
    
    List<RiskRule> findByStatus(RiskRuleStatus status);
    
    @Query("SELECT r FROM RiskRule r WHERE r.isActive = true AND r.status = 'PRODUCTION' ORDER BY r.priority DESC")
    List<RiskRule> findAllActiveRules();
}
public interface RiskEventRepository extends JpaRepository<RiskEvent, Long> {
    Optional<RiskEvent> findByEventId(String eventId);
    
    List<RiskEvent> findByEventTypeAndStatus(RiskEventType eventType, RiskEventStatus status);
    
    List<RiskEvent> findByCustomerIdAndCreatedAtAfter(Long customerId, LocalDateTime date);
    
    @Query("SELECT e FROM RiskEvent e WHERE e.status = 'PENDING' ORDER BY e.createdAt DESC")
    List<RiskEvent> findPendingEvents();
    
    @Query("SELECT e FROM RiskEvent e WHERE e.createdAt BETWEEN :start AND :end")
    List<RiskEvent> findEventsBetweenDates(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end);
}

public interface RiskParameterRepository extends JpaRepository<RiskParameter, Long> {
    Optional<RiskParameter> findByParamKey(String paramKey);
}
public interface RiskBlacklistRepository extends JpaRepository<RiskBlacklist, Long> {
    Optional<RiskBlacklist> findByEntityTypeAndEntityValue(String entityType, String entityValue);
    
    List<RiskBlacklist> findByEntityType(String entityType);
}

网站公告

今日签到

点亮在社区的每一天
去签到