1. 保单管理
保单创建与维护(新保、续保、批改)
保单查询与统计
保单状态跟踪(生效、终止、注销)
保费计算引擎
yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/insurance_db?useSSL=false&serverTimezone=UTC
username: insurance_user
password: securepassword
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
cache:
type: redis
redis:
host: localhost
port: 6379
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
management:
endpoints:
web:
exposure:
include: health,info,metrics
seata:
enabled: true
application-id: insurance-policy-service
tx-service-group: insurance_tx_group
service:
vgroup-mapping:
insurance_tx_group: default
grouplist:
default: 127.0.0.1:8091
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 30000
pool-name: InsuranceHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
server:
tomcat:
max-threads: 200
min-spare-threads: 10
accept-count: 100
java
/**
保单主表
*/
@Entity
@Table(name = "policy")
public class Policy {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String policyNo; // 保单号
@ManyToOne
@JoinColumn(name = "product_id", nullable = false)
private Product product; // 保险产品
@ManyToOne
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer; // 投保人
@Column(nullable = false)
private Date effectiveDate; // 生效日期
@Column(nullable = false)
private Date expiryDate; // 失效日期
@Column(nullable = false)
private BigDecimal premium; // 保费
@Column(nullable = false)
private BigDecimal sumInsured; // 保额
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PolicyStatus status; // 保单状态
@OneToMany(mappedBy = "policy", cascade = CascadeType.ALL)
private List<PolicyBeneficiary> beneficiaries; // 受益人
@OneToMany(mappedBy = "policy", cascade = CascadeType.ALL)
private List<PolicyRider> riders; // 附加险
@Column(nullable = false)
private Date createdAt;
@Column(nullable = false)
private Date updatedAt;
// Getters and Setters
}
java
public enum PolicyStatus {
DRAFT, // 草稿
PENDING, // 待核保
UNDER_REVIEW, // 核保中
ACTIVE, // 生效中
LAPSED, // 已失效
CANCELLED, // 已退保
CLAIMED // 已理赔
}
java
@Data
public class PolicyDTO {
private Long id;
@NotBlank
private String policyNo;
@NotNull
private Long productId;
@NotNull
private Long customerId;
@NotNull
@Future
private Date effectiveDate;
@NotNull
@Future
private Date expiryDate;
@NotNull
@DecimalMin("0.00")
private BigDecimal premium;
@NotNull
@DecimalMin("0.00")
private BigDecimal sumInsured;
private PolicyStatus status;
private List<BeneficiaryDTO> beneficiaries;
private List<PolicyRiderDTO> riders;
}
java
/**
保单号生成器
*/
@Component
public class PolicyNumberGenerator {
private static final String PREFIX = "POL";
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
private static int sequence = 0;
private static final int MAX_SEQUENCE = 9999;
public synchronized String generatePolicyNo(String productCode) {
String datePart = dateFormat.format(new Date());
if (sequence >= MAX_SEQUENCE) {
sequence = 0;
}
sequence++;
String sequencePart = String.format("%04d", sequence);
return PREFIX + productCode + datePart + sequencePart;
}
}
java
@Configuration
@EnableTransactionManagement
@EnableAutoDataSourceProxy
public class SeataConfig {
// Seata自动配置
}
java
@Configuration
public class RabbitMQConfig {
@Bean
public Queue policyQueue() {
return new Queue("policy.queue", true);
}
@Bean
public Queue underwritingQueue() {
return new Queue("underwriting.queue", true);
}
}
java
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(60))
.disableCachingNullValues()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
}
java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtRequestFilter jwtRequestFilter;
public SecurityConfig(JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
JwtRequestFilter jwtRequestFilter) {
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtRequestFilter = jwtRequestFilter;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiResponse> handleResourceNotFound(ResourceNotFoundException ex) {
log.warn("Resource not found: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error(HttpStatus.NOT_FOUND.value(), ex.getMessage()));
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse> handleBusinessException(BusinessException ex) {
log.error("Business exception: {}", ex.getMessage(), ex);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining("; "));
log.warn("Validation error: {}", errorMessage);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), errorMessage));
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse> handleConstraintViolation(ConstraintViolationException ex) {
String errorMessage = ex.getConstraintViolations().stream()
.map(violation -> violation.getPropertyPath() + ": " + violation.getMessage())
.collect(Collectors.joining("; "));
log.warn("Constraint violation: {}", errorMessage);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), errorMessage));
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiResponse> handleAccessDenied(AccessDeniedException ex) {
log.warn("Access denied: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.error(HttpStatus.FORBIDDEN.value(), "无权访问该资源"));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse> handleAllExceptions(Exception ex) {
log.error("Unexpected error", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.value(),
"系统内部错误,请联系管理员"));
}
}
Controller层
java
@RestController
@RequestMapping("/api/policies")
@Api(tags = "保单管理")
public class PolicyController {
private final PolicyService policyService;
public PolicyController(PolicyService policyService) {
this.policyService = policyService;
}
@PostMapping
@ApiOperation("创建保单")
@PreAuthorize("hasRole('UNDERWRITER') or hasRole('ADMIN')")
public ResponseEntity<Policy> createPolicy(@RequestBody PolicyDTO policyDTO) {
Policy policy = policyService.createPolicy(policyDTO);
return ResponseEntity.ok(policy);
}
@PutMapping("/{id}")
@ApiOperation("更新保单")
@PreAuthorize("hasRole('UNDERWRITER') or hasRole('ADMIN')")
public ResponseEntity<Policy> updatePolicy(@PathVariable Long id, @RequestBody PolicyDTO policyDTO) {
Policy policy = policyService.updatePolicy(id, policyDTO);
return ResponseEntity.ok(policy);
}
@DeleteMapping("/{id}")
@ApiOperation("删除保单")
@PreAuthorize("hasRole('UNDERWRITER') or hasRole('ADMIN')")
public ResponseEntity<Void> deletePolicy(@PathVariable Long id) {
policyService.deletePolicy(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}")
@ApiOperation("获取保单详情")
@PreAuthorize("hasAnyRole('AGENT', 'UNDERWRITER', 'CLAIM', 'ADMIN')")
public ResponseEntity<Policy> getPolicyById(@PathVariable Long id) {
Policy policy = policyService.getPolicyById(id);
return ResponseEntity.ok(policy);
}
@GetMapping("/number/{policyNo}")
@ApiOperation("通过保单号获取保单")
@PreAuthorize("hasAnyRole('AGENT', 'UNDERWRITER', 'CLAIM', 'ADMIN')")
public ResponseEntity<Policy> getPolicyByNo(@PathVariable String policyNo) {
Policy policy = policyService.getPolicyByNo(policyNo);
return ResponseEntity.ok(policy);
}
@GetMapping
@ApiOperation("获取所有保单(分页)")
@PreAuthorize("hasAnyRole('AGENT', 'UNDERWRITER', 'CLAIM', 'ADMIN')")
public ResponseEntity<Page<Policy>> getAllPolicies(Pageable pageable) {
Page<Policy> policies = policyService.getAllPolicies(pageable);
return ResponseEntity.ok(policies);
}
@GetMapping("/customer/{customerId}")
@ApiOperation("获取客户的所有保单")
@PreAuthorize("hasAnyRole('AGENT', 'UNDERWRITER', 'CLAIM', 'ADMIN')")
public ResponseEntity<List<Policy>> getPoliciesByCustomer(@PathVariable Long customerId) {
List<Policy> policies = policyService.getPoliciesByCustomer(customerId);
return ResponseEntity.ok(policies);
}
@GetMapping("/status/{status}")
@ApiOperation("根据状态获取保单")
@PreAuthorize("hasAnyRole('UNDERWRITER', 'CLAIM', 'ADMIN')")
public ResponseEntity<List<Policy>> getPoliciesByStatus(@PathVariable String status) {
List<Policy> policies = policyService.getPoliciesByStatus(status);
return ResponseEntity.ok(policies);
}
@GetMapping("/active")
@ApiOperation("获取指定日期的有效保单")
@PreAuthorize("hasAnyRole('UNDERWRITER', 'CLAIM', 'ADMIN')")
public ResponseEntity<List<Policy>> getActivePolicies(@RequestParam(required = false) Date date) {
if (date == null) {
date = new Date();
}
List<Policy> policies = policyService.getActivePolicies(date);
return ResponseEntity.ok(policies);
}
@PutMapping("/{id}/status/{status}")
@ApiOperation("变更保单状态")
@PreAuthorize("hasAnyRole('UNDERWRITER', 'CLAIM', 'ADMIN')")
public ResponseEntity<Policy> changePolicyStatus(@PathVariable Long id, @PathVariable String status) {
Policy policy = policyService.changePolicyStatus(id, status);
return ResponseEntity.ok(policy);
}
@PostMapping("/calculate-premium")
@ApiOperation("计算保费")
@PreAuthorize("hasAnyRole('AGENT', 'UNDERWRITER', 'ADMIN')")
public ResponseEntity<BigDecimal> calculatePremium(@RequestBody PolicyDTO policyDTO) {
BigDecimal premium = policyService.calculatePremium(policyDTO);
return ResponseEntity.ok(premium);
}
}
Service层
java
public interface PolicyService {
Policy createPolicy(PolicyDTO policyDTO);
Policy updatePolicy(Long id, PolicyDTO policyDTO);
void deletePolicy(Long id);
Policy getPolicyById(Long id);
Policy getPolicyByNo(String policyNo);
Page<Policy> getAllPolicies(Pageable pageable);
List<Policy> getPoliciesByCustomer(Long customerId);
List<Policy> getPoliciesByStatus(String status);
List<Policy> getActivePolicies(Date date);
Policy changePolicyStatus(Long id, String status);
BigDecimal calculatePremium(PolicyDTO policyDTO);
}
java
@Service
@Transactional
public class PolicyServiceImpl implements PolicyService {
private final PolicyRepository policyRepository;
private final ProductRepository productRepository;
private final CustomerRepository customerRepository;
private final PolicyBeneficiaryRepository beneficiaryRepository;
private final PolicyRiderRepository riderRepository;
private final ModelMapper modelMapper;
private final RabbitTemplate rabbitTemplate;
private final PolicyNumberGenerator policyNumberGenerator;
public PolicyServiceImpl(PolicyRepository policyRepository,
ProductRepository productRepository,
CustomerRepository customerRepository,
PolicyBeneficiaryRepository beneficiaryRepository,
PolicyRiderRepository riderRepository,
ModelMapper modelMapper,
RabbitTemplate rabbitTemplate,
PolicyNumberGenerator policyNumberGenerator) {
this.policyRepository = policyRepository;
this.productRepository = productRepository;
this.customerRepository = customerRepository;
this.beneficiaryRepository = beneficiaryRepository;
this.riderRepository = riderRepository;
this.modelMapper = modelMapper;
this.rabbitTemplate = rabbitTemplate;
this.policyNumberGenerator = policyNumberGenerator;
}
@Override
@GlobalTransactional
public Policy createPolicy(PolicyDTO policyDTO) {
// 验证产品
Product product = productRepository.findById(policyDTO.getProductId())
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
// 验证客户
Customer customer = customerRepository.findById(policyDTO.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found"));
// 生成保单号
String policyNo = policyNumberGenerator.generatePolicyNo(product.getCode());
// 计算保费
BigDecimal premium = premiumCalculationEngine.calculatePremium(policyDTO, product);
policyDTO.setPremium(premium);
// 创建保单
Policy policy = modelMapper.map(policyDTO, Policy.class);
policy.setPolicyNo(policyNo);
policy.setProduct(product);
policy.setCustomer(customer);
policy.setStatus(PolicyStatus.PENDING);
policy.setUnderwritingStatus(UnderwritingStatus.PENDING);
policy.setCreatedAt(new Date());
policy.setUpdatedAt(new Date());
// 保存保单
Policy savedPolicy = policyRepository.save(policy);
// 保存受益人
saveBeneficiaries(policyDTO, savedPolicy);
// 保存附加险
saveRiders(policyDTO, savedPolicy);
// 记录审计日志
auditLogService.logPolicyCreation(savedPolicy);
// 异步执行核保
performUnderwritingAsync(savedPolicy);
return savedPolicy;
}
@Async
protected void performUnderwritingAsync(Policy policy) {
try {
UnderwritingStatus status = underwritingService.performAutoUnderwriting(policy);
if (status == UnderwritingStatus.APPROVED) {
// 自动核保通过,准备支付
preparePayment(policy);
} else if (status == UnderwritingStatus.REFERRED) {
// 需要人工核保
underwritingService.initiateManualUnderwriting(policy.getId());
}
} catch (Exception e) {
log.error("Underwriting failed for policy: " + policy.getPolicyNo(), e);
}
}
private void preparePayment(Policy policy) {
PaymentRequest paymentRequest = new PaymentRequest();
paymentRequest.setPolicyNo(policy.getPolicyNo());
paymentRequest.setAmount(policy.getPremium());
paymentRequest.setOrderId("PAY-" + policy.getPolicyNo());
PaymentResult result = paymentService.initiatePayment(paymentRequest);
if (result.isSuccess()) {
policy.setPaymentStatus(PaymentStatus.PENDING);
policyRepository.save(policy);
// 发送支付通知
rabbitTemplate.convertAndSend("payment.notification.queue", policy.getId());
}
}
private void saveRiders(PolicyDTO policyDTO, Policy savedPolicy) {
if (policyDTO.getRiders() != null && !policyDTO.getRiders().isEmpty()) {
List<PolicyRider> riders = policyDTO.getRiders().stream()
.map(r -> {
PolicyRider rider = modelMapper.map(r, PolicyRider.class);
rider.setPolicy(savedPolicy);
return rider;
})
.collect(Collectors.toList());
riderRepository.saveAll(riders);
}
}
private void saveBeneficiaries(PolicyDTO policyDTO, Policy savedPolicy) {
if (policyDTO.getBeneficiaries() != null && !policyDTO.getBeneficiaries().isEmpty()) {
List<PolicyBeneficiary> beneficiaries = policyDTO.getBeneficiaries().stream()
.map(b -> {
PolicyBeneficiary beneficiary = modelMapper.map(b, PolicyBeneficiary.class);
beneficiary.setPolicy(savedPolicy);
return beneficiary;
})
.collect(Collectors.toList());
beneficiaryRepository.saveAll(beneficiaries);
}
}
@Override
public Policy createPolicy(PolicyDTO policyDTO) {
// 验证产品是否存在
Product product = productRepository.findById(policyDTO.getProductId())
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
// 验证客户是否存在
Customer customer = customerRepository.findById(policyDTO.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found"));
// 生成保单号
String policyNo = policyNumberGenerator.generatePolicyNo(product.getCode());
// 创建保单
Policy policy = modelMapper.map(policyDTO, Policy.class);
policy.setPolicyNo(policyNo);
policy.setProduct(product);
policy.setCustomer(customer);
policy.setStatus(PolicyStatus.DRAFT);
policy.setCreatedAt(new Date());
policy.setUpdatedAt(new Date());
// 保存保单
Policy savedPolicy = policyRepository.save(policy);
// 保存受益人
if (policyDTO.getBeneficiaries() != null && !policyDTO.getBeneficiaries().isEmpty()) {
List<PolicyBeneficiary> beneficiaries = policyDTO.getBeneficiaries().stream()
.map(b -> {
PolicyBeneficiary beneficiary = modelMapper.map(b, PolicyBeneficiary.class);
beneficiary.setPolicy(savedPolicy);
return beneficiary;
})
.collect(Collectors.toList());
beneficiaryRepository.saveAll(beneficiaries);
}
// 保存附加险
if (policyDTO.getRiders() != null && !policyDTO.getRiders().isEmpty()) {
List<PolicyRider> riders = policyDTO.getRiders().stream()
.map(r -> {
PolicyRider rider = modelMapper.map(r, PolicyRider.class);
rider.setPolicy(savedPolicy);
return rider;
})
.collect(Collectors.toList());
riderRepository.saveAll(riders);
}
// 发送消息到队列,触发后续流程
rabbitTemplate.convertAndSend("policy.queue", savedPolicy.getId());
return savedPolicy;
}
@Override
@CacheEvict(value = "policies", key = "#id")
public Policy updatePolicy(Long id, PolicyDTO policyDTO) {
Policy existingPolicy = policyRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Policy not found"));
// 检查是否可以修改
if (!existingPolicy.getStatus().equals(PolicyStatus.DRAFT)) {
throw new IllegalStateException("Only draft policies can be modified");
}
modelMapper.map(policyDTO, existingPolicy);
existingPolicy.setUpdatedAt(new Date());
// 更新受益人
if (policyDTO.getBeneficiaries() != null) {
// 删除现有受益人
beneficiaryRepository.deleteByPolicyId(id);
// 添加新的受益人
List<PolicyBeneficiary> beneficiaries = policyDTO.getBeneficiaries().stream()
.map(b -> {
PolicyBeneficiary beneficiary = modelMapper.map(b, PolicyBeneficiary.class);
beneficiary.setPolicy(existingPolicy);
return beneficiary;
})
.collect(Collectors.toList());
beneficiaryRepository.saveAll(beneficiaries);
}
// 更新附加险
if (policyDTO.getRiders() != null) {
// 删除现有附加险
riderRepository.deleteByPolicyId(id);
// 添加新的附加险
List<PolicyRider> riders = policyDTO.getRiders().stream()
.map(r -> {
PolicyRider rider = modelMapper.map(r, PolicyRider.class);
rider.setPolicy(existingPolicy);
return rider;
})
.collect(Collectors.toList());
riderRepository.saveAll(riders);
}
return policyRepository.save(existingPolicy);
}
@Override
@CacheEvict(value = "policies", key = "#id")
public void deletePolicy(Long id) {
Policy policy = policyRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Policy not found"));
// 检查是否可以删除
if (!policy.getStatus().equals(PolicyStatus.DRAFT)) {
throw new IllegalStateException("Only draft policies can be deleted");
}
policyRepository.delete(policy);
}
@Override
@Cacheable(value = "policies", key = "#id")
public Policy getPolicyById(Long id) {
return policyRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Policy not found"));
}
@Override
@Cacheable(value = "policies", key = "#policyNo")
public Policy getPolicyByNo(String policyNo) {
return policyRepository.findByPolicyNo(policyNo)
.orElseThrow(() -> new ResourceNotFoundException("Policy not found"));
}
@Override
public Page<Policy> getAllPolicies(Pageable pageable) {
return policyRepository.findAll(pageable);
}
@Override
public List<Policy> getPoliciesByCustomer(Long customerId) {
return policyRepository.findByCustomerId(customerId);
}
@Override
public List<Policy> getPoliciesByStatus(String status) {
PolicyStatus policyStatus = PolicyStatus.valueOf(status.toUpperCase());
return policyRepository.findByStatus(policyStatus);
}
@Override
public List<Policy> getActivePolicies(Date date) {
return policyRepository.findActivePolicies(date);
}
@Override
@CacheEvict(value = "policies", key = "#id")
public Policy changePolicyStatus(Long id, String status) {
Policy policy = policyRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Policy not found"));
PolicyStatus newStatus = PolicyStatus.valueOf(status.toUpperCase());
// 验证状态转换是否合法
validateStatusTransition(policy.getStatus(), newStatus);
policy.setStatus(newStatus);
policy.setUpdatedAt(new Date());
// 如果是激活状态,记录激活时间
if (newStatus == PolicyStatus.ACTIVE) {
policy.setEffectiveDate(new Date());
}
return policyRepository.save(policy);
}
@Override
public BigDecimal calculatePremium(PolicyDTO policyDTO) {
// 获取产品
Product product = productRepository.findById(policyDTO.getProductId())
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
// 基础保费
BigDecimal premium = product.getBasePremium();
// 根据保额调整保费
BigDecimal sumInsuredFactor = policyDTO.getSumInsured()
.divide(product.getBaseSumInsured(), 4, BigDecimal.ROUND_HALF_UP);
premium = premium.multiply(sumInsuredFactor);
// 计算附加险保费
if (policyDTO.getRiders() != null) {
for (PolicyRiderDTO rider : policyDTO.getRiders()) {
// 这里应该有更复杂的计算逻辑
premium = premium.add(rider.getPremium());
}
}
return premium.setScale(2, BigDecimal.ROUND_HALF_UP);
}
private void validateStatusTransition(PolicyStatus currentStatus, PolicyStatus newStatus) {
// 这里应该有完整的业务规则来验证状态转换是否合法
// 简化版示例
if (currentStatus == PolicyStatus.DRAFT && newStatus != PolicyStatus.PENDING) {
throw new IllegalStateException("Draft can only transition to Pending");
}
if (currentStatus == PolicyStatus.ACTIVE &&
(newStatus != PolicyStatus.LAPSED && newStatus != PolicyStatus.CANCELLED)) {
throw new IllegalStateException("Active policy can only be lapsed or cancelled");
}
}
}
java
/**
复杂引擎保费计算
*/
@Service
public class PremiumCalculationEngine{
private final KieContainer kieContainer;
private final ProductRiskEvaluator riskEvaluator;
public PremiumCalculationEngine(KieContainer kieContainer,
ProductRiskEvaluator riskEvaluator) {
this.kieContainer = kieContainer;
this.riskEvaluator = riskEvaluator;
}
public BigDecimal calculatePremium(PolicyDTO policyDTO, Product product) {
KieSession kieSession = kieContainer.newKieSession();
PremiumCalculationContext context = new PremiumCalculationContext();
context.setBasePremium(product.getBasePremium());
context.setSumInsured(policyDTO.getSumInsured());
context.setBaseSumInsured(product.getBaseSumInsured());
context.setProductType(product.getType());
context.setRiskLevel(riskEvaluator.evaluateRisk(policyDTO, product));
// 设置附加险信息
if (policyDTO.getRiders() != null) {
context.setRiderCount(policyDTO.getRiders().size());
}
kieSession.insert(context);
kieSession.fireAllRules();
kieSession.dispose();
return context.getFinalPremium();
}
}
java
@Data
public class PremiumCalculationContext {
private BigDecimal basePremium;
private BigDecimal sumInsured;
private BigDecimal baseSumInsured;
private String productType;
private String riskLevel;
private int riderCount;
private BigDecimal finalPremium;
// 其他计算因子...
}
drl
package com.insurance.premium.calculation
import com.insurance.service.calculation.PremiumCalculationContext;
rule "Basic premium calculation"
when
$context : PremiumCalculationContext()
then
// 基础保费计算
BigDecimal sumInsuredFactor = $context.getSumInsured().divide(
$context.getBaseSumInsured(), 4, java.math.RoundingMode.HALF_UP);
$context.setFinalPremium($context.getBasePremium().multiply(sumInsuredFactor));
end
rule "High risk surcharge"
when
$context : PremiumCalculationContext(riskLevel == "HIGH")
then
// 高风险附加费
$context.setFinalPremium($context.getFinalPremium().multiply(new BigDecimal("1.2")));
end
rule "Multiple riders discount"
when
$context : PremiumCalculationContext(riderCount >= 3)
then
// 多个附加险折扣
$context.setFinalPremium($context.getFinalPremium().multiply(new BigDecimal("0.95")));
end
java
/**
核保流程集成
*/
public interface UnderwritingService {
UnderwritingStatus performAutoUnderwriting(Policy policy);
void initiateManualUnderwriting(Long policyId);
UnderwritingStatus checkUnderwritingStatus(Long policyId);
}
java
@Service
public class UnderwritingServiceImpl implements UnderwritingService {
private final PolicyRepository policyRepository;
private final RabbitTemplate rabbitTemplate;
private final UnderwritingRuleEngine ruleEngine;
public UnderwritingServiceImpl(PolicyRepository policyRepository,
RabbitTemplate rabbitTemplate,
UnderwritingRuleEngine ruleEngine) {
this.policyRepository = policyRepository;
this.rabbitTemplate = rabbitTemplate;
this.ruleEngine = ruleEngine;
}
@Override
@Transactional
public UnderwritingStatus performAutoUnderwriting(Policy policy) {
UnderwritingStatus status = ruleEngine.evaluatePolicy(policy);
policy.setUnderwritingStatus(status);
policyRepository.save(policy);
if (status == UnderwritingStatus.REFERRED) {
rabbitTemplate.convertAndSend("underwriting.queue", policy.getId());
}
return status;
}
@Override
public void initiateManualUnderwriting(Long policyId) {
rabbitTemplate.convertAndSend("manual.underwriting.queue", policyId);
}
@Override
public UnderwritingStatus checkUnderwritingStatus(Long policyId) {
return policyRepository.findById(policyId)
.map(Policy::getUnderwritingStatus)
.orElse(UnderwritingStatus.PENDING);
}
}
java
/**
支付集成
*/
public interface PaymentService {
PaymentResult initiatePayment(PaymentRequest request);
boolean verifyPayment(String paymentId);
void processPaymentCallback(String callbackData);
}
java
@Service("alipayPaymentService")
@Slf4j
public class AlipayPaymentServiceImpl implements PaymentService {
private final AlipayClient alipayClient;
@Value("${alipay.return-url}")
private String returnUrl;
@Value("${alipay.notify-url}")
private String notifyUrl;
public AlipayPaymentServiceImpl(AlipayClient alipayClient) {
this.alipayClient = alipayClient;
}
@Override
public PaymentResult initiatePayment(PaymentRequest request) {
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(returnUrl);
alipayRequest.setNotifyUrl(notifyUrl);
String subject = "保险产品支付 - 保单号: " + request.getPolicyNo();
alipayRequest.setBizContent("{" +
"\"out_trade_no\":\"" + request.getOrderId() + "\"," +
"\"total_amount\":\"" + request.getAmount() + "\"," +
"\"subject\":\"" + subject + "\"," +
"\"product_code\":\"FAST_INSTANT_TRADE_PAY\"" +
"}");
try {
AlipayTradePagePayResponse response = alipayClient.pageExecute(alipayRequest);
if (response.isSuccess()) {
return new PaymentResult(true, response.getBody(), "支付发起成功");
} else {
log.error("支付宝支付发起失败: {}", response.getSubMsg());
return new PaymentResult(false, null, response.getSubMsg());
}
} catch (AlipayApiException e) {
log.error("支付宝支付异常", e);
return new PaymentResult(false, null, "支付系统异常");
}
}
@Override
public boolean verifyPayment(String paymentId) {
// 实现支付验证逻辑
return true;
}
@Override
public void processPaymentCallback(String callbackData) {
// 处理支付回调
}
}
Repository层
java
public interface PolicyRepository extends JpaRepository<Policy, Long> {
Policy findByPolicyNo(String policyNo);
List<Policy> findByCustomerId(Long customerId);
List<Policy> findByStatus(PolicyStatus status);
@Query("SELECT p FROM Policy p WHERE p.effectiveDate <= :date AND p.expiryDate >= :date")
List<Policy> findActivePolicies(@Param("date") Date date);
@Query("SELECT COUNT(p) FROM Policy p WHERE p.product.id = :productId AND p.status = 'ACTIVE'")
Long countActivePoliciesByProduct(@Param("productId") Long productId);
}
2. 客户管理
客户信息管理(个人/企业客户)
客户分级与标签
客户360度视图
客户沟通记录
yml
# Aliyun OCR配置
aliyun:
ocr:
access-key-id: your-access-key-id
access-key-secret: your-access-key-secret
endpoint: ocr-api.cn-hangzhou.aliyuncs.com
# Elasticsearch配置
spring:
elasticsearch:
rest:
uris: http://localhost:9200
username: elastic
password: yourpassword
# 缓存配置
cache:
customer:
ttl: 3600 # 1小时
java
@Entity
@Table(name = "customer")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private CustomerType type; // 个人或企业
@Column(unique = true)
private String idNumber; // 身份证/统一社会信用代码
@Column(unique = true)
private String mobile;
@Column(unique = true)
private String email;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private List<Address> addresses;
@OneToMany(mappedBy = "customer")
private List<Policy> policies;
@Enumerated(EnumType.STRING)
private CustomerLevel level; // 客户等级
@Column(nullable = false)
private Date createdAt;
@Column(nullable = false)
private Date updatedAt;
private Date birthday;
private String gender;
private String occupation;
// 企业客户特有字段
private String legalRepresentative;
private String businessScope;
private Date establishmentDate;
// Getters and Setters
}
java
public enum CustomerType {
INDIVIDUAL, // 个人客户
CORPORATE // 企业客户
}
public enum CustomerLevel {
REGULAR, // 普通客户
SILVER, // 银牌客户
GOLD, // 金牌客户
PLATINUM, // 白金客户
DIAMOND // 钻石客户
}
java
@Entity
@Table(name = "address")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
@Column(nullable = false)
private String province;
@Column(nullable = false)
private String city;
@Column(nullable = false)
private String district;
@Column(nullable = false)
private String detail;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private AddressType type; // 家庭地址、工作地址等
@Column(nullable = false)
private Boolean isDefault;
// Getters and Setters
}
java
@Data
public class CustomerDTO {
private Long id;
@NotBlank
@Size(min = 2, max = 50)
private String name;
@NotNull
private CustomerType type;
@NotBlank
private String idNumber;
@NotBlank
@Pattern(regexp = "^1[3-9]\\d{9}$")
private String mobile;
@Email
private String email;
private CustomerLevel level;
private Date birthday;
private String gender;
private String occupation;
// 企业客户特有字段
private String legalRepresentative;
private String businessScope;
private Date establishmentDate;
@NotNull
@Size(min = 1)
private List<AddressDTO> addresses;
@Data
public static class AddressDTO {
private Long id;
@NotBlank
private String province;
@NotBlank
private String city;
@NotBlank
private String district;
@NotBlank
private String detail;
@NotBlank
private String type;
@NotNull
private Boolean isDefault;
}
}
java
@Data
public class CustomerSearchDTO {
private String keyword;
private CustomerType type;
private CustomerLevel level;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date startDate;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date endDate;
private String mobile;
private String idNumber;
}
工具
java
public class ExcelHelper {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static String getCellStringValue(Cell cell) {
if (cell == null) {
return "";
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue().trim();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
Date date = cell.getDateCellValue();
return dateFormat.format(date);
} else {
return String.valueOf((long) cell.getNumericCellValue());
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
return cell.getCellFormula();
default:
return "";
}
}
}
java
@Component
public class IdCardOCRUtil {
@Value("${aliyun.ocr.access-key-id}")
private String accessKeyId;
@Value("${aliyun.ocr.access-key-secret}")
private String accessKeySecret;
@Value("${aliyun.ocr.endpoint}")
private String endpoint;
public boolean verifyIdCard(String idNumber, String name,
MultipartFile frontImage, MultipartFile backImage) {
try {
// 初始化客户端
com.aliyun.ocr_api20210707.Client client = createClient();
// 验证正面
RecognizeIdcardRequest frontRequest = new RecognizeIdcardRequest()
.setSide("face")
.setImageURL(""); // 实际使用中需要上传到OSS获取URL
RecognizeIdcardResponse frontResponse = client.recognizeIdcard(frontRequest);
String frontIdNumber = frontResponse.getBody().getData().getCardNumber();
String frontName = frontResponse.getBody().getData().getName();
// 验证背面
RecognizeIdcardRequest backRequest = new RecognizeIdcardRequest()
.setSide("back")
.setImageURL(""); // 实际使用中需要上传到OSS获取URL
client.recognizeIdcard(backRequest);
// 验证信息是否匹配
return idNumber.equals(frontIdNumber) && name.equals(frontName);
} catch (Exception e) {
throw new RuntimeException("身份证OCR验证失败", e);
}
}
private com.aliyun.ocr_api20210707.Client createClient() throws Exception {
Config config = new Config()
.setAccessKeyId(accessKeyId)
.setAccessKeySecret(accessKeySecret);
config.endpoint = endpoint;
return new com.aliyun.ocr_api20210707.Client(config);
}
}
Controller层
java
@RestController
@RequestMapping("/api/customers")
@Api(tags = "客户管理")
public class CustomerController {
private final CustomerService customerService;
public CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
@PostMapping
@ApiOperation("创建客户")
@PreAuthorize("hasRole('AGENT') or hasRole('ADMIN')")
public ResponseEntity<Customer> createCustomer(@RequestBody CustomerDTO customerDTO) {
Customer customer = customerService.createCustomer(customerDTO);
return ResponseEntity.ok(customer);
}
@PutMapping("/{id}")
@ApiOperation("更新客户信息")
@PreAuthorize("hasRole('AGENT') or hasRole('ADMIN')")
public ResponseEntity<Customer> updateCustomer(@PathVariable Long id,
@RequestBody CustomerDTO customerDTO) {
Customer customer = customerService.updateCustomer(id, customerDTO);
return ResponseEntity.ok(customer);
}
@DeleteMapping("/{id}")
@ApiOperation("删除客户")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteCustomer(@PathVariable Long id) {
customerService.deleteCustomer(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}")
@ApiOperation("获取客户详情")
@PreAuthorize("hasAnyRole('AGENT', 'UNDERWRITER', 'CLAIM', 'ADMIN')")
public ResponseEntity<Customer> getCustomerById(@PathVariable Long id) {
Customer customer = customerService.getCustomerById(id);
return ResponseEntity.ok(customer);
}
@GetMapping("/id-number/{idNumber}")
@ApiOperation("通过身份证号获取客户")
@PreAuthorize("hasAnyRole('AGENT', 'UNDERWRITER', 'CLAIM', 'ADMIN')")
public ResponseEntity<Customer> getCustomerByIdNumber(@PathVariable String idNumber) {
Customer customer = customerService.getCustomerByIdNumber(idNumber);
return ResponseEntity.ok(customer);
}
@GetMapping("/search")
@ApiOperation("搜索客户")
@PreAuthorize("hasAnyRole('AGENT', 'UNDERWRITER', 'CLAIM', 'ADMIN')")
public ResponseEntity<Page<Customer>> searchCustomers(CustomerSearchDTO searchDTO,
Pageable pageable) {
Page<Customer> customers = customerService.searchCustomers(searchDTO, pageable);
return ResponseEntity.ok(customers);
}
@GetMapping("/level/{level}")
@ApiOperation("根据等级获取客户列表")
@PreAuthorize("hasAnyRole('AGENT', 'ADMIN')")
public ResponseEntity<List<Customer>> getCustomersByLevel(@PathVariable String level) {
List<Customer> customers = customerService.getCustomersByLevel(level);
return ResponseEntity.ok(customers);
}
@PostMapping("/import")
@ApiOperation("导入客户Excel")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> importCustomers(@RequestParam("file") MultipartFile file) {
customerService.importCustomersFromExcel(file);
return ResponseEntity.ok().build();
}
@GetMapping("/export")
@ApiOperation("导出客户Excel")
@PreAuthorize("hasAnyRole('AGENT', 'ADMIN')")
public ResponseEntity<byte[]> exportCustomers() {
byte[] excelData = customerService.exportCustomersToExcel();
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=customers.xlsx")
.body(excelData);
}
@PostMapping("/verify-id-card")
@ApiOperation("身份证验证")
public ResponseEntity<Customer> verifyIdCard(
@RequestParam String idNumber,
@RequestParam String name,
@RequestParam MultipartFile frontImage,
@RequestParam MultipartFile backImage) {
Customer customer = customerService.verifyIdCard(idNumber, name, frontImage, backImage);
return ResponseEntity.ok(customer);
}
}
Service层
java
@Slf4j
@Service
@Transactional
public class CustomerServiceImpl implements CustomerService {
private final CustomerRepository customerRepository;
private final AddressRepository addressRepository;
private final CustomerSearchRepository customerSearchRepository;
private final ModelMapper modelMapper;
private final IdCardOCRUtil idCardOCRUtil;
private final AuditLogService auditLogService;
@Autowired
public CustomerServiceImpl(CustomerRepository customerRepository,
AddressRepository addressRepository,
CustomerSearchRepository customerSearchRepository,
ModelMapper modelMapper,
IdCardOCRUtil idCardOCRUtil,
AuditLogService auditLogService) {
this.customerRepository = customerRepository;
this.addressRepository = addressRepository;
this.customerSearchRepository = customerSearchRepository;
this.modelMapper = modelMapper;
this.idCardOCRUtil = idCardOCRUtil;
this.auditLogService = auditLogService;
}
@Override
@CacheEvict(value = "customers", allEntries = true)
public Customer createCustomer(CustomerDTO customerDTO) {
// 验证身份证号是否已存在
if (customerRepository.existsByIdNumber(customerDTO.getIdNumber())) {
throw new BusinessException("该身份证号已注册");
}
// 验证手机号是否已存在
if (customerRepository.existsByMobile(customerDTO.getMobile())) {
throw new BusinessException("该手机号已注册");
}
// 创建客户
Customer customer = modelMapper.map(customerDTO, Customer.class);
customer.setCreatedAt(new Date());
customer.setUpdatedAt(new Date());
// 设置默认客户等级
if (customer.getLevel() == null) {
customer.setLevel(CustomerLevel.REGULAR);
}
Customer savedCustomer = customerRepository.save(customer);
// 保存地址
saveAddresses(customerDTO, savedCustomer);
// 记录审计日志
auditLogService.logCustomerCreation(savedCustomer);
// 异步同步到Elasticsearch
syncCustomerToElasticsearchAsync(savedCustomer);
return savedCustomer;
}
@Override
@CacheEvict(value = "customers", key = "#id")
public Customer updateCustomer(Long id, CustomerDTO customerDTO) {
Customer existingCustomer = customerRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("客户不存在"));
// 验证身份证号是否已被其他客户使用
if (!existingCustomer.getIdNumber().equals(customerDTO.getIdNumber()) {
if (customerRepository.existsByIdNumber(customerDTO.getIdNumber())) {
throw new BusinessException("该身份证号已被其他客户使用");
}
}
// 验证手机号是否已被其他客户使用
if (!existingCustomer.getMobile().equals(customerDTO.getMobile())) {
if (customerRepository.existsByMobile(customerDTO.getMobile())) {
throw new BusinessException("该手机号已被其他客户使用");
}
}
modelMapper.map(customerDTO, existingCustomer);
existingCustomer.setUpdatedAt(new Date());
// 更新地址
updateAddresses(customerDTO, existingCustomer);
Customer updatedCustomer = customerRepository.save(existingCustomer);
// 异步同步到Elasticsearch
syncCustomerToElasticsearchAsync(updatedCustomer);
return updatedCustomer;
}
@Override
@CacheEvict(value = "customers", key = "#id")
public void deleteCustomer(Long id) {
Customer customer = customerRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("客户不存在"));
// 检查是否有关联保单
if (!customer.getPolicies().isEmpty()) {
throw new BusinessException("该客户有关联保单,无法删除");
}
// 删除地址
addressRepository.deleteByCustomerId(id);
customerRepository.delete(customer);
// 从Elasticsearch删除
deleteCustomerFromElasticsearchAsync(id);
}
@Override
@Cacheable(value = "customers", key = "#id")
public Customer getCustomerById(Long id) {
return customerRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("客户不存在"));
}
@Override
@Cacheable(value = "customers", key = "#idNumber")
public Customer getCustomerByIdNumber(String idNumber) {
return customerRepository.findByIdNumber(idNumber)
.orElseThrow(() -> new ResourceNotFoundException("客户不存在"));
}
@Override
public Page<Customer> searchCustomers(CustomerSearchDTO searchDTO, Pageable pageable) {
return customerRepository.searchCustomers(
searchDTO.getKeyword(),
searchDTO.getType(),
searchDTO.getLevel(),
searchDTO.getStartDate(),
searchDTO.getEndDate(),
pageable);
}
@Override
public List<Customer> getCustomersByLevel(String level) {
CustomerLevel customerLevel = CustomerLevel.valueOf(level.toUpperCase());
return customerRepository.findByLevel(customerLevel);
}
@Override
public void importCustomersFromExcel(MultipartFile file) {
try {
Workbook workbook = new XSSFWorkbook(file.getInputStream());
Sheet sheet = workbook.getSheetAt(0);
List<Customer> customers = new ArrayList<>();
for (Row row : sheet) {
if (row.getRowNum() == 0) continue; // 跳过标题行
CustomerDTO dto = new CustomerDTO();
dto.setName(ExcelHelper.getCellStringValue(row.getCell(0)));
dto.setType(CustomerType.valueOf(ExcelHelper.getCellStringValue(row.getCell(1))));
dto.setIdNumber(ExcelHelper.getCellStringValue(row.getCell(2)));
dto.setMobile(ExcelHelper.getCellStringValue(row.getCell(3)));
dto.setEmail(ExcelHelper.getCellStringValue(row.getCell(4)));
// 创建客户
createCustomer(dto);
}
workbook.close();
} catch (IOException e) {
log.error("导入客户Excel失败", e);
throw new BusinessException("导入客户Excel失败");
}
}
@Override
public byte[] exportCustomersToExcel() {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
Sheet sheet = workbook.createSheet("客户列表");
// 创建标题行
Row headerRow = sheet.createRow(0);
String[] headers = {"姓名", "类型", "身份证号", "手机号", "邮箱", "等级", "创建时间"};
for (int i = 0; i < headers.length; i++) {
headerRow.createCell(i).setCellValue(headers[i]);
}
// 填充数据
List<Customer> customers = customerRepository.findAll();
int rowNum = 1;
for (Customer customer : customers) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(customer.getName());
row.createCell(1).setCellValue(customer.getType().name());
row.createCell(2).setCellValue(customer.getIdNumber());
row.createCell(3).setCellValue(customer.getMobile());
row.createCell(4).setCellValue(customer.getEmail());
row.createCell(5).setCellValue(customer.getLevel().name());
row.createCell(6).setCellValue(customer.getCreatedAt().toString());
}
workbook.write(out);
return out.toByteArray();
} catch (IOException e) {
log.error("导出客户Excel失败", e);
throw new BusinessException("导出客户Excel失败");
}
}
@Override
@Async
public void syncCustomersToElasticsearch() {
Iterable<Customer> customers = customerRepository.findAll();
customerSearchRepository.saveAll(customers);
log.info("同步所有客户数据到Elasticsearch完成");
}
@Override
public Customer verifyIdCard(String idNumber, String name, MultipartFile frontImage, MultipartFile backImage) {
// 使用OCR验证身份证
boolean verified = idCardOCRUtil.verifyIdCard(idNumber, name, frontImage, backImage);
if (!verified) {
throw new BusinessException("身份证验证失败");
}
// 查找或创建客户
Customer customer = customerRepository.findByIdNumber(idNumber)
.orElseGet(() -> {
CustomerDTO dto = new CustomerDTO();
dto.setName(name);
dto.setType(CustomerType.INDIVIDUAL);
dto.setIdNumber(idNumber);
dto.setMobile(""); // 需要后续补充
return createCustomer(dto);
});
// 标记为已验证
customer.setIdVerified(true);
customerRepository.save(customer);
return customer;
}
private void saveAddresses(CustomerDTO customerDTO, Customer customer) {
for (CustomerDTO.AddressDTO addressDTO : customerDTO.getAddresses()) {
Address address = modelMapper.map(addressDTO, Address.class);
address.setCustomer(customer);
addressRepository.save(address);
}
}
private void updateAddresses(CustomerDTO customerDTO, Customer customer) {
// 删除旧地址
addressRepository.deleteByCustomerId(customer.getId());
// 添加新地址
saveAddresses(customerDTO, customer);
}
@Async
protected void syncCustomerToElasticsearchAsync(Customer customer) {
try {
customerSearchRepository.save(customer);
log.info("同步客户数据到Elasticsearch: {}", customer.getId());
} catch (Exception e) {
log.error("同步客户到Elasticsearch失败: {}", customer.getId(), e);
}
}
@Async
protected void deleteCustomerFromElasticsearchAsync(Long customerId) {
try {
customerSearchRepository.deleteById(customerId);
log.info("从Elasticsearch删除客户: {}", customerId);
} catch (Exception e) {
log.error("从Elasticsearch删除客户失败: {}", customerId, e);
}
}
}
Repository层
java
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByIdNumber(String idNumber);
Optional<Customer> findByMobile(String mobile);
List<Customer> findByType(CustomerType type);
List<Customer> findByLevel(CustomerLevel level);
@Query("SELECT c FROM Customer c WHERE " +
"(:keyword IS NULL OR c.name LIKE %:keyword% OR c.mobile LIKE %:keyword%) AND " +
"(:type IS NULL OR c.type = :type) AND " +
"(:level IS NULL OR c.level = :level) AND " +
"(:startDate IS NULL OR c.createdAt >= :startDate) AND " +
"(:endDate IS NULL OR c.createdAt <= :endDate)")
Page<Customer> searchCustomers(@Param("keyword") String keyword,
@Param("type") CustomerType type,
@Param("level") CustomerLevel level,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
Pageable pageable);
@Query("SELECT COUNT(c) FROM Customer c WHERE c.createdAt BETWEEN :start AND :end")
Long countByCreateDateBetween(@Param("start") Date start, @Param("end") Date end);
}
java
@Repository
public interface CustomerSearchRepository extends ElasticsearchRepository<Customer, Long> {
List<Customer> findByNameOrMobileOrIdNumber(String name, String mobile, String idNumber);
List<Customer> findByLevel(String level);
}
3. 产品管理
保险产品配置(条款、费率、规则)
产品生命周期管理
产品组合与打包
java
@Entity
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String code; // 产品代码
@Column(nullable = false)
private String name; // 产品名称
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ProductType type; // 产品类型
@Column(nullable = false)
private String description;
@Column(nullable = false)
private BigDecimal basePremium; // 基础保费
@Column(nullable = false)
private BigDecimal baseSumInsured; // 基础保额
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ProductStatus status; // 产品状态
@Column(nullable = false)
private Date effectiveDate; // 生效日期
@Column(nullable = false)
private Date expiryDate; // 失效日期
@Column(nullable = false)
private Integer minAge; // 最小投保年龄
@Column(nullable = false)
private Integer maxAge; // 最大投保年龄
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
private List<ProductRider> riders; // 可选附加险
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
private List<ProductClause> clauses; // 条款
@OneToMany(mappedBy = "product")
private List<Policy> policies; // 关联保单
@Column(nullable = false)
private Date createdAt;
@Column(nullable = false)
private Date updatedAt;
@Version
private Long version; // 乐观锁版本号
// Getters and Setters
}
java
public enum ProductType {
LIFE, // 寿险
HEALTH, // 健康险
ACCIDENT, // 意外险
PROPERTY, // 财产险
LIABILITY, // 责任险
TRAVEL, // 旅行险
ANNUITY // 年金险
}
java
public enum ProductStatus {
DRAFT, // 草稿
PENDING, // 待审核
APPROVED, // 已审核
ACTIVE, // 已生效
INACTIVE, // 已停售
ARCHIVED // 已归档
}
java
@Entity
@Table(name = "product_rider")
public class ProductRider {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@Column(nullable = false)
private String code;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String description;
@Column(nullable = false)
private BigDecimal premium; // 附加险保费
@Column(nullable = false)
private BigDecimal sumInsured; // 附加险保额
@Column(nullable = false)
private Boolean isDefault; // 是否默认选择
// Getters and Setters
}
java
@Entity
@Table(name = "product_clause")
public class ProductClause {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@Column(nullable = false)
private String clauseNo; // 条款编号
@Column(nullable = false)
private String title; // 条款标题
@Lob
@Column(nullable = false)
private String content; // 条款内容
@Column(nullable = false)
private Integer displayOrder; // 显示顺序
// Getters and Setters
}
java
@Data
public class ProductDTO {
private Long id;
@NotBlank
@Size(min = 2, max = 20)
private String code;
@NotBlank
@Size(min = 2, max = 100)
private String name;
@NotNull
private ProductType type;
@NotBlank
@Size(min = 10, max = 1000)
private String description;
@NotNull
@DecimalMin("0.00")
private BigDecimal basePremium;
@NotNull
@DecimalMin("0.00")
private BigDecimal baseSumInsured;
private ProductStatus status;
@NotNull
@Future
private Date effectiveDate;
@NotNull
@Future
private Date expiryDate;
@NotNull
@Min(0)
@Max(120)
private Integer minAge;
@NotNull
@Min(0)
@Max(120)
private Integer maxAge;
@NotNull
@Size(min = 1)
private List<ProductRiderDTO> riders;
@NotNull
@Size(min = 1)
private List<ProductClauseDTO> clauses;
@Data
public static class ProductRiderDTO {
private Long id;
@NotBlank
private String code;
@NotBlank
private String name;
@NotBlank
private String description;
@NotNull
@DecimalMin("0.00")
private BigDecimal premium;
@NotNull
@DecimalMin("0.00")
private BigDecimal sumInsured;
@NotNull
private Boolean isDefault;
}
@Data
public static class ProductClauseDTO {
private Long id;
@NotBlank
private String clauseNo;
@NotBlank
private String title;
@NotBlank
private String content;
@NotNull
@Min(1)
private Integer displayOrder;
}
}
java
@Data
public class ProductSearchDTO {
private String keyword;
private ProductType type;
private ProductStatus status;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date effectiveDateFrom;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date effectiveDateTo;
private Integer minAge;
private Integer maxAge;
}
java
/**
DroolsRuleGenerator工具
*/
@Component
public class DroolsRuleGenerator {
public String generateProductRules(Product product) {
StringBuilder rules = new StringBuilder();
// 包声明
rules.append(MessageFormat.format(
"package com.insurance.rules.product.{0};\n\n", product.getCode().toLowerCase()));
// 导入
rules.append("import com.insurance.dto.PolicyDTO;\n");
rules.append("import com.insurance.service.calculation.PremiumCalculationContext;\n");
rules.append("import java.math.BigDecimal;\n\n");
// 规则1: 基础保费计算
rules.append(MessageFormat.format(
"rule \"Base premium calculation - {0}\"\n", product.getCode()));
rules.append("when\n");
rules.append(" $context : PremiumCalculationContext( productCode == \"" + product.getCode() + "\" )\n");
rules.append("then\n");
rules.append(MessageFormat.format(
" BigDecimal sumInsuredFactor = $context.getSumInsured().divide(new BigDecimal(\"{0}\"), 4, BigDecimal.ROUND_HALF_UP);\n",
product.getBaseSumInsured()));
rules.append(MessageFormat.format(
" $context.setFinalPremium(new BigDecimal(\"{0}\").multiply(sumInsuredFactor));\n",
product.getBasePremium()));
rules.append("end\n\n");
// 规则2: 年龄限制
rules.append(MessageFormat.format(
"rule \"Age restriction - {0}\"\n", product.getCode()));
rules.append("when\n");
rules.append(" $context : PremiumCalculationContext( productCode == \"" + product.getCode() + "\" )\n");
rules.append(MessageFormat.format(
" PolicyDTO( applicantAge < {0} || applicantAge > {1} )\n",
product.getMinAge(), product.getMaxAge()));
rules.append("then\n");
rules.append(" throw new RuntimeException(\"投保年龄不符合产品要求\");\n");
rules.append("end\n\n");
// 为每个附加险生成规则
for (ProductRider rider : product.getRiders()) {
rules.append(MessageFormat.format(
"rule \"Rider premium - {0} - {1}\"\n", product.getCode(), rider.getCode()));
rules.append("when\n");
rules.append(" $context : PremiumCalculationContext( productCode == \"" + product.getCode() + "\" )\n");
rules.append(MessageFormat.format(
" PolicyDTO( riders contains \"{0}\" )\n", rider.getCode()));
rules.append("then\n");
rules.append(MessageFormat.format(
" $context.setFinalPremium($context.getFinalPremium().add(new BigDecimal(\"{0}\")));\n",
rider.getPremium()));
rules.append("end\n\n");
}
return rules.toString();
}
}
Controller层
java
@RestController
@RequestMapping("/api/products")
@Api(tags = "产品管理")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@PostMapping
@ApiOperation("创建产品")
@PreAuthorize("hasRole('PRODUCT_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<Product> createProduct(@RequestBody ProductDTO productDTO) {
Product product = productService.createProduct(productDTO);
return ResponseEntity.ok(product);
}
@PutMapping("/{id}")
@ApiOperation("更新产品")
@PreAuthorize("hasRole('PRODUCT_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@RequestBody ProductDTO productDTO) {
Product product = productService.updateProduct(id, productDTO);
return ResponseEntity.ok(product);
}
@DeleteMapping("/{id}")
@ApiOperation("删除产品")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}")
@ApiOperation("获取产品详情")
@PreAuthorize("hasAnyRole('AGENT', 'UNDERWRITER', 'PRODUCT_MANAGER', 'ADMIN')")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}
@GetMapping("/code/{code}")
@ApiOperation("通过产品代码获取产品")
@PreAuthorize("hasAnyRole('AGENT', 'UNDERWRITER', 'PRODUCT_MANAGER', 'ADMIN')")
public ResponseEntity<Product> getProductByCode(@PathVariable String code) {
Product product = productService.getProductByCode(code);
return ResponseEntity.ok(product);
}
@GetMapping("/search")
@ApiOperation("搜索产品")
@PreAuthorize("hasAnyRole('AGENT', 'UNDERWRITER', 'PRODUCT_MANAGER', 'ADMIN')")
public ResponseEntity<Page<Product>> searchProducts(ProductSearchDTO searchDTO,
Pageable pageable) {
Page<Product> products = productService.searchProducts(searchDTO, pageable);
return ResponseEntity.ok(products);
}
@GetMapping("/active")
@ApiOperation("获取有效产品列表")
@PreAuthorize("hasAnyRole('AGENT', 'UNDERWRITER', 'PRODUCT_MANAGER', 'ADMIN')")
public ResponseEntity<List<Product>> getActiveProducts() {
List<Product> products = productService.getActiveProducts();
return ResponseEntity.ok(products);
}
@PutMapping("/{id}/status/{status}")
@ApiOperation("变更产品状态")
@PreAuthorize("hasRole('PRODUCT_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<Product> changeProductStatus(@PathVariable Long id,
@PathVariable String status) {
Product product = productService.changeProductStatus(id, status);
return ResponseEntity.ok(product);
}
@GetMapping("/expiring")
@ApiOperation("获取即将过期的产品")
@PreAuthorize("hasRole('PRODUCT_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<List<Product>> findProductsToExpire() {
List<Product> products = productService.findProductsToExpire();
return ResponseEntity.ok(products);
}
@PostMapping("/expire")
@ApiOperation("批量停用产品")
@PreAuthorize("hasRole('PRODUCT_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<Void> expireProducts(@RequestBody List<Long> productIds) {
productService.expireProducts(productIds);
return ResponseEntity.noContent().build();
}
@PostMapping("/sync-to-es")
@ApiOperation("同步产品到Elasticsearch")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> syncProductsToElasticsearch() {
productService.syncProductsToElasticsearch();
return ResponseEntity.noContent().build();
}
}
Service层
java
public interface ProductService {
Product createProduct(ProductDTO productDTO);
Product updateProduct(Long id, ProductDTO productDTO);
void deleteProduct(Long id);
Product getProductById(Long id);
Product getProductByCode(String code);
Page<Product> searchProducts(ProductSearchDTO searchDTO, Pageable pageable);
List<Product> getActiveProducts();
Product changeProductStatus(Long id, String status);
List<Product> findProductsToExpire();
void expireProducts(List<Long> productIds);
void syncProductsToElasticsearch();
}
java
@Slf4j
@Service
@Transactional
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
private final ProductRiderRepository riderRepository;
private final ProductClauseRepository clauseRepository;
private final ProductSearchRepository productSearchRepository;
private final ModelMapper modelMapper;
private final DroolsRuleGenerator droolsRuleGenerator;
private final AuditLogService auditLogService;
public ProductServiceImpl(ProductRepository productRepository,
ProductRiderRepository riderRepository,
ProductClauseRepository clauseRepository,
ProductSearchRepository productSearchRepository,
ModelMapper modelMapper,
DroolsRuleGenerator droolsRuleGenerator,
AuditLogService auditLogService) {
this.productRepository = productRepository;
this.riderRepository = riderRepository;
this.clauseRepository = clauseRepository;
this.productSearchRepository = productSearchRepository;
this.modelMapper = modelMapper;
this.droolsRuleGenerator = droolsRuleGenerator;
this.auditLogService = auditLogService;
}
@Override
@CacheEvict(value = "products", allEntries = true)
public Product createProduct(ProductDTO productDTO) {
// 验证产品代码是否已存在
if (productRepository.existsByCode(productDTO.getCode())) {
throw new BusinessException("产品代码已存在");
}
// 创建产品
Product product = modelMapper.map(productDTO, Product.class);
product.setStatus(ProductStatus.DRAFT);
product.setCreatedAt(new Date());
product.setUpdatedAt(new Date());
Product savedProduct = productRepository.save(product);
// 保存附加险
saveRiders(productDTO, savedProduct);
// 保存条款
saveClauses(productDTO, savedProduct);
// 生成Drools规则
generateDroolsRules(savedProduct);
// 记录审计日志
auditLogService.logProductCreation(savedProduct);
// 异步同步到Elasticsearch
syncProductToElasticsearchAsync(savedProduct);
return savedProduct;
}
@Override
@CacheEvict(value = "products", key = "#id")
public Product updateProduct(Long id, ProductDTO productDTO) {
Product existingProduct = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("产品不存在"));
// 验证产品代码是否已被其他产品使用
if (!existingProduct.getCode().equals(productDTO.getCode())) {
if (productRepository.existsByCode(productDTO.getCode())) {
throw new BusinessException("产品代码已被其他产品使用");
}
}
// 检查是否可以修改
if (existingProduct.getStatus() != ProductStatus.DRAFT &&
existingProduct.getStatus() != ProductStatus.PENDING) {
throw new BusinessException("只有草稿或待审核状态的产品可以修改");
}
modelMapper.map(productDTO, existingProduct);
existingProduct.setUpdatedAt(new Date());
// 更新附加险
updateRiders(productDTO, existingProduct);
// 更新条款
updateClauses(productDTO, existingProduct);
Product updatedProduct = productRepository.save(existingProduct);
// 重新生成Drools规则
generateDroolsRules(updatedProduct);
// 异步同步到Elasticsearch
syncProductToElasticsearchAsync(updatedProduct);
return updatedProduct;
}
@Override
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("产品不存在"));
// 检查是否有关联保单
if (!product.getPolicies().isEmpty()) {
throw new BusinessException("该产品有关联保单,无法删除");
}
// 删除附加险
riderRepository.deleteByProductId(id);
// 删除条款
clauseRepository.deleteByProductId(id);
productRepository.delete(product);
// 从Elasticsearch删除
deleteProductFromElasticsearchAsync(id);
}
@Override
@Cacheable(value = "products", key = "#id")
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("产品不存在"));
}
@Override
@Cacheable(value = "products", key = "#code")
public Product getProductByCode(String code) {
return productRepository.findByCode(code)
.orElseThrow(() -> new ResourceNotFoundException("产品不存在"));
}
@Override
public Page<Product> searchProducts(ProductSearchDTO searchDTO, Pageable pageable) {
return productRepository.searchProducts(
searchDTO.getKeyword(),
searchDTO.getType(),
searchDTO.getStatus(),
searchDTO.getEffectiveDateFrom(),
searchDTO.getEffectiveDateTo(),
searchDTO.getMinAge(),
searchDTO.getMaxAge(),
pageable);
}
@Override
public List<Product> getActiveProducts() {
return productRepository.findActiveProducts(new Date());
}
@Override
@CacheEvict(value = "products", key = "#id")
public Product changeProductStatus(Long id, String status) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("产品不存在"));
ProductStatus newStatus = ProductStatus.valueOf(status.toUpperCase());
// 验证状态转换是否合法
validateStatusTransition(product.getStatus(), newStatus);
product.setStatus(newStatus);
product.setUpdatedAt(new Date());
Product updatedProduct = productRepository.save(product);
// 异步同步到Elasticsearch
syncProductToElasticsearchAsync(updatedProduct);
return updatedProduct;
}
@Override
public List<Product> findProductsToExpire() {
return productRepository.findProductsToExpire(new Date());
}
@Override
@CacheEvict(value = "products", allEntries = true)
public void expireProducts(List<Long> productIds) {
List<Product> products = productRepository.findAllById(productIds);
products.forEach(product -> {
product.setStatus(ProductStatus.INACTIVE);
product.setUpdatedAt(new Date());
productRepository.save(product);
// 异步同步到Elasticsearch
syncProductToElasticsearchAsync(product);
});
}
@Override
@Async
public void syncProductsToElasticsearch() {
Iterable<Product> products = productRepository.findAll();
productSearchRepository.saveAll(products);
log.info("同步所有产品数据到Elasticsearch完成");
}
@Scheduled(cron = "0 0 0 * * ?") // 每天午夜执行
public void autoUpdateProductStatus() {
Date today = new Date();
// 激活到期的产品
List<Product> productsToActivate = productRepository
.findByStatusAndEffectiveDateLessThanEqual(ProductStatus.APPROVED, today);
productsToActivate.forEach(product -> {
product.setStatus(ProductStatus.ACTIVE);
productRepository.save(product);
syncProductToElasticsearchAsync(product);
});
// 停用过期的产品
List<Product> productsToExpire = productRepository.findProductsToExpire(today);
productsToExpire.forEach(product -> {
product.setStatus(ProductStatus.INACTIVE);
productRepository.save(product);
syncProductToElasticsearchAsync(product);
});
}
private void saveRiders(ProductDTO productDTO, Product product) {
for (ProductDTO.ProductRiderDTO riderDTO : productDTO.getRiders()) {
ProductRider rider = modelMapper.map(riderDTO, ProductRider.class);
rider.setProduct(product);
riderRepository.save(rider);
}
}
private void updateRiders(ProductDTO productDTO, Product product) {
// 删除旧附加险
riderRepository.deleteByProductId(product.getId());
// 添加新附加险
saveRiders(productDTO, product);
}
private void saveClauses(ProductDTO productDTO, Product product) {
for (ProductDTO.ProductClauseDTO clauseDTO : productDTO.getClauses()) {
ProductClause clause = modelMapper.map(clauseDTO, ProductClause.class);
clause.setProduct(product);
clauseRepository.save(clause);
}
}
private void updateClauses(ProductDTO productDTO, Product product) {
// 删除旧条款
clauseRepository.deleteByProductId(product.getId());
// 添加新条款
saveClauses(productDTO, product);
}
private void generateDroolsRules(Product product) {
String ruleContent = droolsRuleGenerator.generateProductRules(product);
// 保存规则到文件或数据库,供Drools引擎加载
log.info("Generated Drools rules for product {}:\n{}", product.getCode(), ruleContent);
}
private void validateStatusTransition(ProductStatus currentStatus, ProductStatus newStatus) {
// 这里应该有完整的业务规则来验证状态转换是否合法
// 简化版示例
if (currentStatus == ProductStatus.DRAFT &&
!(newStatus == ProductStatus.PENDING || newStatus == ProductStatus.DRAFT)) {
throw new BusinessException("草稿状态只能转为待审核状态");
}
if (currentStatus == ProductStatus.ACTIVE && newStatus != ProductStatus.INACTIVE) {
throw new BusinessException("生效产品只能停售");
}
}
@Async
protected void syncProductToElasticsearchAsync(Product product) {
try {
productSearchRepository.save(product);
log.info("同步产品数据到Elasticsearch: {}", product.getId());
} catch (Exception e) {
log.error("同步产品到Elasticsearch失败: {}", product.getId(), e);
}
}
@Async
protected void deleteProductFromElasticsearchAsync(Long productId) {
try {
productSearchRepository.deleteById(productId);
log.info("从Elasticsearch删除产品: {}", productId);
} catch (Exception e) {
log.error("从Elasticsearch删除产品失败: {}", productId, e);
}
}
}
Repository层
java
public interface ProductRepository extends JpaRepository<Product, Long> {
Optional<Product> findByCode(String code);
List<Product> findByType(ProductType type);
List<Product> findByStatus(ProductStatus status);
@Query("SELECT p FROM Product p WHERE " +
"(:keyword IS NULL OR p.name LIKE %:keyword% OR p.code LIKE %:keyword%) AND " +
"(:type IS NULL OR p.type = :type) AND " +
"(:status IS NULL OR p.status = :status) AND " +
"(:effectiveDateFrom IS NULL OR p.effectiveDate >= :effectiveDateFrom) AND " +
"(:effectiveDateTo IS NULL OR p.effectiveDate <= :effectiveDateTo) AND " +
"(:minAge IS NULL OR p.minAge <= :minAge) AND " +
"(:maxAge IS NULL OR p.maxAge >= :maxAge)")
Page<Product> searchProducts(@Param("keyword") String keyword,
@Param("type") ProductType type,
@Param("status") ProductStatus status,
@Param("effectiveDateFrom") Date effectiveDateFrom,
@Param("effectiveDateTo") Date effectiveDateTo,
@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge,
Pageable pageable);
@Query("SELECT p FROM Product p WHERE p.effectiveDate <= :date AND p.expiryDate >= :date AND p.status = 'ACTIVE'")
List<Product> findActiveProducts(@Param("date") Date date);
@Query("SELECT p FROM Product p WHERE p.expiryDate < :date AND p.status = 'ACTIVE'")
List<Product> findProductsToExpire(@Param("date") Date date);
}
java
@Repository
public interface ProductSearchRepository extends ElasticsearchRepository<Product, Long> {
List<Product> findByNameOrCode(String name, String code);
List<Product> findByType(String type);
List<Product> findByStatus(String status);
}
4. 核保管理
自动核保规则引擎
人工核保工作流
风险评估模型
再保管理
yml
# Activiti配置
activiti:
check-process-definitions: true
database-schema-update: true
history-level: full
async-executor-activate: true
# Drools配置
drools:
kie-container-id: underwritingKieContainer
# 缓存配置
cache:
underwriting:
ttl: 3600 # 1小时
工作流配置Activiti
xml
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:activiti="http://activiti.org/bpmn"
typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath"
targetNamespace="http://www.activiti.org/processdef">
<process id="underwritingProcess" name="Underwriting Process" isExecutable="true">
<startEvent id="startEvent" name="Start"/>
<sequenceFlow id="flow1" sourceRef="startEvent" targetRef="autoUnderwritingTask"/>
<serviceTask id="autoUnderwritingTask" name="Auto Underwriting"
activiti:class="com.insurance.workflow.AutoUnderwritingDelegate"/>
<sequenceFlow id="flow2" sourceRef="autoUnderwritingTask" targetRef="decisionGateway"/>
<exclusiveGateway id="decisionGateway" name="Underwriting Decision"/>
<sequenceFlow id="flowApproved" sourceRef="decisionGateway" targetRef="approvedEndEvent">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${decision == 'APPROVED'}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flowDeclined" sourceRef="decisionGateway" targetRef="declinedEndEvent">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${decision == 'DECLINED'}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flowReferred" sourceRef="decisionGateway" targetRef="manualUnderwritingTask">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${decision == 'REFERRED'}]]>
</conditionExpression>
</sequenceFlow>
<userTask id="manualUnderwritingTask" name="Manual Underwriting"
activiti:candidateGroups="underwriters">
<documentation>Manual underwriting task</documentation>
</userTask>
<sequenceFlow id="flow3" sourceRef="manualUnderwritingTask" targetRef="decisionGateway2"/>
<exclusiveGateway id="decisionGateway2" name="Manual Underwriting Decision"/>
<sequenceFlow id="flow4" sourceRef="decisionGateway2" targetRef="approvedEndEvent">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${decision == 'APPROVED'}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow5" sourceRef="decisionGateway2" targetRef="declinedEndEvent">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${decision == 'DECLINED'}]]>
</conditionExpression>
</sequenceFlow>
<endEvent id="approvedEndEvent" name="Approved">
<terminateEventDefinition/>
</endEvent>
<endEvent id="declinedEndEvent" name="Declined">
<terminateEventDefinition/>
</endEvent>
</process>
</definitions>
java
@Component
public class AutoUnderwritingDelegate implements JavaDelegate {
private final UnderwritingService underwritingService;
@Autowired
public AutoUnderwritingDelegate(UnderwritingService underwritingService) {
this.underwritingService = underwritingService;
}
@Override
public void execute(DelegateExecution execution) {
Long policyId = (Long) execution.getVariable("policyId");
underwritingService.performAutoUnderwriting(policyId);
// 设置决策变量
execution.setVariable("decision", execution.getVariable("decision"));
execution.setVariable("decisionReason", execution.getVariable("decisionReason"));
}
}
规则引擎
java
@Component
public class DroolsUnderwritingEngine {
private final KieContainer kieContainer;
public DroolsUnderwritingEngine(KieContainer kieContainer) {
this.kieContainer = kieContainer;
}
public UnderwritingDecision evaluate(Policy policy, List<UnderwritingRule> rules) {
KieSession kieSession = kieContainer.newKieSession();
// 设置决策上下文
UnderwritingDecision decision = new UnderwritingDecision();
decision.setStatus(UnderwritingStatus.PENDING);
// 插入事实
kieSession.insert(policy);
kieSession.insert(decision);
rules.forEach(kieSession::insert);
// 执行规则
kieSession.fireAllRules();
kieSession.dispose();
return decision;
}
public static class UnderwritingDecision {
private UnderwritingStatus status;
private String reason;
// Getters and Setters
}
}
java
@Data
public class UnderwritingRuleDTO {
private Long id;
@NotNull
private Long productId;
@NotBlank
private String ruleName;
@NotBlank
private String ruleExpression;
@NotBlank
private String decision;
@NotNull
private Integer priority;
@NotNull
private Boolean isActive;
}
java
@Data
public class UnderwritingDecisionDTO {
@NotNull
private Long underwritingId;
@NotNull
private UnderwritingStatus decision;
@NotBlank
private String reason;
private String notes;
}
java
@Data
public class UnderwritingDTO {
private Long id;
@NotNull
private Long policyId;
@NotNull
private UnderwritingStatus status;
private UnderwritingType type;
private String decisionReason;
private String assignedTo;
private String notes;
@NotNull
private Date createdAt;
@NotNull
private Date updatedAt;
private Date completedAt;
}
java
@Entity
@Table(name = "underwriting_rule")
public class UnderwritingRule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "product_id", nullable = false)
private Product product; // 关联产品
@Column(nullable = false)
private String ruleName; // 规则名称
@Column(nullable = false)
private String ruleExpression; // 规则表达式
@Column(nullable = false)
private String decision; // 决策结果(通过/拒绝/转人工)
@Column(nullable = false)
private Integer priority; // 优先级
@Column(nullable = false)
private Boolean isActive; // 是否激活
// Getters and Setters
}
java
public enum UnderwritingType {
AUTO, // 自动核保
MANUAL // 人工核保
}
public enum UnderwritingStatus {
PENDING, // 待核保
IN_PROGRESS, // 核保中
APPROVED, // 已通过
DECLINED, // 已拒绝
REFERRED, // 需进一步核保
CANCELLED // 已取消
}
java
@Entity
@Table(name = "underwriting")
public class Underwriting {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
@JoinColumn(name = "policy_id", nullable = false)
private Policy policy; // 关联保单
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UnderwritingStatus status; // 核保状态
@Enumerated(EnumType.STRING)
private UnderwritingType type; // 核保类型(自动/人工)
@Column(length = 2000)
private String decisionReason; // 核保结论原因
@Column
private String assignedTo; // 分配给哪个核保员
@Column(nullable = false)
private Date createdAt;
@Column(nullable = false)
private Date updatedAt;
@Column
private Date completedAt; // 完成时间
@Column(length = 2000)
private String notes; // 核保备注
// Getters and Setters
}
Controller层
java
@RestController
@RequestMapping("/api/underwritings")
@Api(tags = "核保管理")
public class UnderwritingController {
private final UnderwritingService underwritingService;
public UnderwritingController(UnderwritingService underwritingService) {
this.underwritingService = underwritingService;
}
@PostMapping("/initiate/{policyId}")
@ApiOperation("发起核保")
@PreAuthorize("hasRole('UNDERWRITER') or hasRole('ADMIN')")
public ResponseEntity<Underwriting> initiateUnderwriting(@PathVariable Long policyId) {
Underwriting underwriting = underwritingService.initiateUnderwriting(policyId);
return ResponseEntity.ok(underwriting);
}
@GetMapping("/{id}")
@ApiOperation("获取核保详情")
@PreAuthorize("hasAnyRole('UNDERWRITER', 'ADMIN')")
public ResponseEntity<Underwriting> getUnderwritingById(@PathVariable Long id) {
Underwriting underwriting = underwritingService.getUnderwritingById(id);
return ResponseEntity.ok(underwriting);
}
@GetMapping("/policy/{policyId}")
@ApiOperation("通过保单ID获取核保记录")
@PreAuthorize("hasAnyRole('UNDERWRITER', 'ADMIN')")
public ResponseEntity<Underwriting> getUnderwritingByPolicyId(@PathVariable Long policyId) {
Underwriting underwriting = underwritingService.getUnderwritingByPolicyId(policyId);
return ResponseEntity.ok(underwriting);
}
@GetMapping("/search")
@ApiOperation("搜索核保记录")
@PreAuthorize("hasAnyRole('UNDERWRITER', 'ADMIN')")
public ResponseEntity<Page<Underwriting>> searchUnderwritings(UnderwritingSearchDTO searchDTO,
Pageable pageable) {
Page<Underwriting> underwritings = underwritingService.searchUnderwritings(searchDTO, pageable);
return ResponseEntity.ok(underwritings);
}
@PostMapping("/decision")
@ApiOperation("核保决策")
@PreAuthorize("hasRole('UNDERWRITER') or hasRole('ADMIN')")
public ResponseEntity<Underwriting> makeDecision(@RequestBody UnderwritingDecisionDTO decisionDTO) {
Underwriting underwriting = underwritingService.makeDecision(decisionDTO);
return ResponseEntity.ok(underwriting);
}
@PutMapping("/{id}/assign/{assignee}")
@ApiOperation("分配核保任务")
@PreAuthorize("hasRole('UNDERWRITER_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<Underwriting> assignUnderwriting(@PathVariable Long id,
@PathVariable String assignee) {
Underwriting underwriting = underwritingService.assignUnderwriting(id, assignee);
return ResponseEntity.ok(underwriting);
}
@PostMapping("/rules")
@ApiOperation("创建核保规则")
@PreAuthorize("hasRole('UNDERWRITER_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<UnderwritingRule> createUnderwritingRule(@RequestBody UnderwritingRuleDTO ruleDTO) {
UnderwritingRule rule = underwritingService.createUnderwritingRule(ruleDTO);
return ResponseEntity.ok(rule);
}
@PutMapping("/rules/{id}")
@ApiOperation("更新核保规则")
@PreAuthorize("hasRole('UNDERWRITER_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<UnderwritingRule> updateUnderwritingRule(@PathVariable Long id,
@RequestBody UnderwritingRuleDTO ruleDTO) {
UnderwritingRule rule = underwritingService.updateUnderwritingRule(id, ruleDTO);
return ResponseEntity.ok(rule);
}
@DeleteMapping("/rules/{id}")
@ApiOperation("删除核保规则")
@PreAuthorize("hasRole('UNDERWRITER_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<Void> deleteUnderwritingRule(@PathVariable Long id) {
underwritingService.deleteUnderwritingRule(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/rules/product/{productId}")
@ApiOperation("获取产品核保规则")
@PreAuthorize("hasAnyRole('UNDERWRITER', 'UNDERWRITER_MANAGER', 'ADMIN')")
public ResponseEntity<List<UnderwritingRule>> getRulesByProduct(@PathVariable Long productId) {
List<UnderwritingRule> rules = underwritingService.getRulesByProduct(productId);
return ResponseEntity.ok(rules);
}
@GetMapping("/statistics")
@ApiOperation("获取核保统计")
@PreAuthorize("hasAnyRole('UNDERWRITER_MANAGER', 'ADMIN')")
public ResponseEntity<UnderwritingStatisticsDTO> getUnderwritingStatistics(DateRangeDTO dateRange) {
UnderwritingStatisticsDTO statistics = underwritingService.getUnderwritingStatistics(dateRange);
return ResponseEntity.ok(statistics);
}
}
Service层
java
public interface UnderwritingService {
Underwriting initiateUnderwriting(Long policyId);
Underwriting getUnderwritingById(Long id);
Underwriting getUnderwritingByPolicyId(Long policyId);
Page<Underwriting> searchUnderwritings(UnderwritingSearchDTO searchDTO, Pageable pageable);
Underwriting makeDecision(UnderwritingDecisionDTO decisionDTO);
Underwriting assignUnderwriting(Long id, String assignee);
UnderwritingRule createUnderwritingRule(UnderwritingRuleDTO ruleDTO);
UnderwritingRule updateUnderwritingRule(Long id, UnderwritingRuleDTO ruleDTO);
void deleteUnderwritingRule(Long id);
List<UnderwritingRule> getRulesByProduct(Long productId);
UnderwritingStatus performAutoUnderwriting(Long policyId);
UnderwritingStatisticsDTO getUnderwritingStatistics(DateRangeDTO dateRange);
}
java
@Slf4j
@Service
@Transactional
public class UnderwritingServiceImpl implements UnderwritingService {
private final UnderwritingRepository underwritingRepository;
private final UnderwritingRuleRepository ruleRepository;
private final PolicyRepository policyRepository;
private final ProductRepository productRepository;
private final ModelMapper modelMapper;
private final DroolsUnderwritingEngine droolsEngine;
private final RuntimeService runtimeService;
private final TaskService taskService;
private final RabbitTemplate rabbitTemplate;
private final AuditLogService auditLogService;
public UnderwritingServiceImpl(UnderwritingRepository underwritingRepository,
UnderwritingRuleRepository ruleRepository,
PolicyRepository policyRepository,
ProductRepository productRepository,
ModelMapper modelMapper,
DroolsUnderwritingEngine droolsEngine,
RuntimeService runtimeService,
TaskService taskService,
RabbitTemplate rabbitTemplate,
AuditLogService auditLogService) {
this.underwritingRepository = underwritingRepository;
this.ruleRepository = ruleRepository;
this.policyRepository = policyRepository;
this.productRepository = productRepository;
this.modelMapper = modelMapper;
this.droolsEngine = droolsEngine;
this.runtimeService = runtimeService;
this.taskService = taskService;
this.rabbitTemplate = rabbitTemplate;
this.auditLogService = auditLogService;
}
@Override
@Transactional
public Underwriting initiateUnderwriting(Long policyId) {
Policy policy = policyRepository.findById(policyId)
.orElseThrow(() -> new ResourceNotFoundException("保单不存在"));
// 检查是否已有核保记录
if (underwritingRepository.existsByPolicyId(policyId)) {
throw new BusinessException("该保单已存在核保记录");
}
// 创建核保记录
Underwriting underwriting = new Underwriting();
underwriting.setPolicy(policy);
underwriting.setStatus(UnderwritingStatus.PENDING);
underwriting.setCreatedAt(new Date());
underwriting.setUpdatedAt(new Date());
Underwriting savedUnderwriting = underwritingRepository.save(underwriting);
// 启动工作流
startUnderwritingWorkflow(savedUnderwriting);
// 记录审计日志
auditLogService.logUnderwritingInitiation(savedUnderwriting);
// 异步执行自动核保
performAutoUnderwritingAsync(policyId);
return savedUnderwriting;
}
@Override
@Cacheable(value = "underwritings", key = "#id")
public Underwriting getUnderwritingById(Long id) {
return underwritingRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("核保记录不存在"));
}
@Override
@Cacheable(value = "underwritings", key = "'policy_' + #policyId")
public Underwriting getUnderwritingByPolicyId(Long policyId) {
return underwritingRepository.findByPolicyId(policyId);
}
@Override
public Page<Underwriting> searchUnderwritings(UnderwritingSearchDTO searchDTO, Pageable pageable) {
return underwritingRepository.searchUnderwritings(
searchDTO.getStatus(),
searchDTO.getAssignedTo(),
searchDTO.getStartDate(),
searchDTO.getEndDate(),
pageable);
}
@Override
@CacheEvict(value = "underwritings", key = "#decisionDTO.underwritingId")
public Underwriting makeDecision(UnderwritingDecisionDTO decisionDTO) {
Underwriting underwriting = underwritingRepository.findById(decisionDTO.getUnderwritingId())
.orElseThrow(() -> new ResourceNotFoundException("核保记录不存在"));
// 检查状态是否可以做出决策
if (underwriting.getStatus() != UnderwritingStatus.IN_PROGRESS &&
underwriting.getStatus() != UnderwritingStatus.REFERRED) {
throw new BusinessException("当前状态不能做出核保决策");
}
underwriting.setStatus(decisionDTO.getDecision());
underwriting.setDecisionReason(decisionDTO.getReason());
underwriting.setNotes(decisionDTO.getNotes());
underwriting.setCompletedAt(new Date());
underwriting.setUpdatedAt(new Date());
Underwriting updatedUnderwriting = underwritingRepository.save(underwriting);
// 完成工作流任务
completeWorkflowTask(updatedUnderwriting);
// 根据决策结果处理保单
handlePolicyAfterDecision(updatedUnderwriting);
// 记录审计日志
auditLogService.logUnderwritingDecision(updatedUnderwriting);
return updatedUnderwriting;
}
@Override
@CacheEvict(value = "underwritings", key = "#id")
public Underwriting assignUnderwriting(Long id, String assignee) {
Underwriting underwriting = underwritingRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("核保记录不存在"));
// 检查状态是否可以分配
if (underwriting.getStatus() != UnderwritingStatus.PENDING &&
underwriting.getStatus() != UnderwritingStatus.REFERRED) {
throw new BusinessException("当前状态不能分配核保任务");
}
underwriting.setAssignedTo(assignee);
underwriting.setStatus(UnderwritingStatus.IN_PROGRESS);
underwriting.setUpdatedAt(new Date());
Underwriting updatedUnderwriting = underwritingRepository.save(underwriting);
// 更新工作流任务
updateWorkflowAssignment(updatedUnderwriting);
return updatedUnderwriting;
}
@Override
public UnderwritingRule createUnderwritingRule(UnderwritingRuleDTO ruleDTO) {
Product product = productRepository.findById(ruleDTO.getProductId())
.orElseThrow(() -> new ResourceNotFoundException("产品不存在"));
UnderwritingRule rule = modelMapper.map(ruleDTO, UnderwritingRule.class);
rule.setProduct(product);
return ruleRepository.save(rule);
}
@Override
public UnderwritingRule updateUnderwritingRule(Long id, UnderwritingRuleDTO ruleDTO) {
UnderwritingRule existingRule = ruleRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("核保规则不存在"));
modelMapper.map(ruleDTO, existingRule);
return ruleRepository.save(existingRule);
}
@Override
public void deleteUnderwritingRule(Long id) {
ruleRepository.deleteById(id);
}
@Override
public List<UnderwritingRule> getRulesByProduct(Long productId) {
return ruleRepository.findByProductId(productId);
}
@Override
@Transactional
public UnderwritingStatus performAutoUnderwriting(Long policyId) {
Policy policy = policyRepository.findById(policyId)
.orElseThrow(() -> new ResourceNotFoundException("保单不存在"));
Underwriting underwriting = underwritingRepository.findByPolicyId(policyId);
if (underwriting == null) {
underwriting = initiateUnderwriting(policyId);
}
// 获取产品相关的激活规则
List<UnderwritingRule> rules = ruleRepository.findByProductIdAndIsActiveTrue(
policy.getProduct().getId());
// 执行自动核保
UnderwritingDecision decision = droolsEngine.evaluate(policy, rules);
// 更新核保记录
underwriting.setType(UnderwritingType.AUTO);
underwriting.setStatus(decision.getStatus());
underwriting.setDecisionReason(decision.getReason());
underwriting.setUpdatedAt(new Date());
if (decision.getStatus() != UnderwritingStatus.PENDING &&
decision.getStatus() != UnderwritingStatus.REFERRED) {
underwriting.setCompletedAt(new Date());
}
Underwriting updatedUnderwriting = underwritingRepository.save(underwriting);
// 根据决策结果处理
if (decision.getStatus() == UnderwritingStatus.APPROVED) {
// 自动核保通过,更新保单状态
policy.setStatus(PolicyStatus.UNDERWRITTEN);
policyRepository.save(policy);
// 触发后续流程
rabbitTemplate.convertAndSend("policy.underwritten.queue", policyId);
} else if (decision.getStatus() == UnderwritingStatus.REFERRED) {
// 需要人工核保
rabbitTemplate.convertAndSend("underwriting.manual.queue", policyId);
}
return decision.getStatus();
}
@Override
public UnderwritingStatisticsDTO getUnderwritingStatistics(DateRangeDTO dateRange) {
UnderwritingStatisticsDTO statistics = new UnderwritingStatisticsDTO();
// 获取已通过的核保数量
statistics.setApprovedCount(
underwritingRepository.countApprovedBetween(dateRange.getStartDate(), dateRange.getEndDate()));
// 其他统计指标...
return statistics;
}
private void startUnderwritingWorkflow(Underwriting underwriting) {
Map<String, Object> variables = new HashMap<>();
variables.put("underwritingId", underwriting.getId());
variables.put("policyId", underwriting.getPolicy().getId());
variables.put("productCode", underwriting.getPolicy().getProduct().getCode());
runtimeService.startProcessInstanceByKey("underwritingProcess", variables);
}
private void completeWorkflowTask(Underwriting underwriting) {
Task task = taskService.createTaskQuery()
.processInstanceBusinessKey(underwriting.getId().toString())
.singleResult();
if (task != null) {
Map<String, Object> variables = new HashMap<>();
variables.put("decision", underwriting.getStatus().name());
variables.put("decisionReason", underwriting.getDecisionReason());
taskService.complete(task.getId(), variables);
}
}
private void updateWorkflowAssignment(Underwriting underwriting) {
Task task = taskService.createTaskQuery()
.processInstanceBusinessKey(underwriting.getId().toString())
.singleResult();
if (task != null) {
taskService.setAssignee(task.getId(), underwriting.getAssignedTo());
}
}
private void handlePolicyAfterDecision(Underwriting underwriting) {
Policy policy = underwriting.getPolicy();
switch (underwriting.getStatus()) {
case APPROVED:
policy.setStatus(PolicyStatus.UNDERWRITTEN);
rabbitTemplate.convertAndSend("policy.underwritten.queue", policy.getId());
break;
case DECLINED:
policy.setStatus(PolicyStatus.DECLINED);
rabbitTemplate.convertAndSend("policy.declined.queue", policy.getId());
break;
case REFERRED:
rabbitTemplate.convertAndSend("underwriting.referred.queue", policy.getId());
break;
}
policyRepository.save(policy);
}
@Async
protected void performAutoUnderwritingAsync(Long policyId) {
try {
performAutoUnderwriting(policyId);
} catch (Exception e) {
log.error("自动核保失败: {}", policyId, e);
}
}
}
Repository层
java
public interface UnderwritingRepository extends JpaRepository<Underwriting, Long> {
Underwriting findByPolicyId(Long policyId);
List<Underwriting> findByStatus(UnderwritingStatus status);
List<Underwriting> findByAssignedTo(String assignedTo);
@Query("SELECT u FROM Underwriting u WHERE " +
"u.status = :status AND " +
"(:assignedTo IS NULL OR u.assignedTo = :assignedTo) AND " +
"(:startDate IS NULL OR u.createdAt >= :startDate) AND " +
"(:endDate IS NULL OR u.createdAt <= :endDate)")
Page<Underwriting> searchUnderwritings(@Param("status") UnderwritingStatus status,
@Param("assignedTo") String assignedTo,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
Pageable pageable);
@Query("SELECT COUNT(u) FROM Underwriting u WHERE u.status = 'APPROVED' AND u.completedAt BETWEEN :start AND :end")
Long countApprovedBetween(@Param("start") Date start, @Param("end") Date end);
}
java
public interface UnderwritingRuleRepository extends JpaRepository<UnderwritingRule, Long> {
List<UnderwritingRule> findByProductId(Long productId);
List<UnderwritingRule> findByProductIdAndIsActiveTrue(Long productId);
}
5. 理赔管理
理赔申请受理
理赔调查与审核
理赔计算与支付
欺诈检测
yml
# MinIO配置
minio:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
bucket-name: insurance-claims
# 缓存配置
cache:
claim:
ttl: 86400 # 24小时
# Activiti配置
activiti:
check-process-definitions: true
database-schema-update: true
history-level: full
async-executor-activate: true
java
@Component
public class ClaimNumberGenerator {
private static final String PREFIX = "CLM";
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
private static int sequence = 0;
private static final int MAX_SEQUENCE = 9999;
public synchronized String generateClaimNo(String policyNo) {
String datePart = dateFormat.format(new Date());
if (sequence >= MAX_SEQUENCE) {
sequence = 0;
}
sequence++;
String sequencePart = String.format("%04d", sequence);
return PREFIX + policyNo.substring(3, 7) + datePart + sequencePart;
}
}
java
@Service
public class FileStorageService {
private final MinioClient minioClient;
@Value("${minio.bucket-name}")
private String bucketName;
public FileStorageService(MinioClient minioClient) {
this.minioClient = minioClient;
}
public String storeFile(MultipartFile file) {
try {
// 确保存储桶存在
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!found) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
// 生成唯一文件名
String fileName = UUID.randomUUID().toString() + "-" + file.getOriginalFilename();
// 上传文件
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
return fileName;
} catch (Exception e) {
throw new RuntimeException("文件存储失败", e);
}
}
public byte[] getFile(String fileName) {
try {
GetObjectResponse response = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.build());
return response.readAllBytes();
} catch (Exception e) {
throw new RuntimeException("文件读取失败", e);
}
}
}
Controller层
java
@RestController
@RequestMapping("/api/claims")
@Api(tags = "理赔管理")
public class ClaimController {
private final ClaimService claimService;
public ClaimController(ClaimService claimService) {
this.claimService = claimService;
}
@PostMapping
@ApiOperation("创建理赔")
@PreAuthorize("hasAnyRole('CLAIM', 'ADMIN')")
public ResponseEntity<Claim> createClaim(@RequestBody ClaimDTO claimDTO) {
Claim claim = claimService.createClaim(claimDTO);
return ResponseEntity.ok(claim);
}
@PutMapping("/{id}")
@ApiOperation("更新理赔")
@PreAuthorize("hasAnyRole('CLAIM', 'ADMIN')")
public ResponseEntity<Claim> updateClaim(@PathVariable Long id,
@RequestBody ClaimDTO claimDTO) {
Claim claim = claimService.updateClaim(id, claimDTO);
return ResponseEntity.ok(claim);
}
@GetMapping("/{id}")
@ApiOperation("获取理赔详情")
@PreAuthorize("hasAnyRole('CLAIM', 'ADMIN')")
public ResponseEntity<Claim> getClaimById(@PathVariable Long id) {
Claim claim = claimService.getClaimById(id);
return ResponseEntity.ok(claim);
}
@GetMapping("/number/{claimNo}")
@ApiOperation("通过理赔单号获取理赔")
@PreAuthorize("hasAnyRole('CLAIM', 'ADMIN')")
public ResponseEntity<Claim> getClaimByNo(@PathVariable String claimNo) {
Claim claim = claimService.getClaimByNo(claimNo);
return ResponseEntity.ok(claim);
}
@GetMapping("/search")
@ApiOperation("搜索理赔")
@PreAuthorize("hasAnyRole('CLAIM', 'ADMIN')")
public ResponseEntity<Page<Claim>> searchClaims(ClaimSearchDTO searchDTO,
Pageable pageable) {
Page<Claim> claims = claimService.searchClaims(searchDTO, pageable);
return ResponseEntity.ok(claims);
}
@GetMapping("/policy/{policyId}")
@ApiOperation("获取保单的理赔记录")
@PreAuthorize("hasAnyRole('CLAIM', 'ADMIN')")
public ResponseEntity<List<Claim>> getClaimsByPolicy(@PathVariable Long policyId) {
List<Claim> claims = claimService.getClaimsByPolicy(policyId);
return ResponseEntity.ok(claims);
}
@PostMapping("/{claimId}/submit")
@ApiOperation("提交理赔")
@PreAuthorize("hasAnyRole('CLAIM', 'ADMIN')")
public ResponseEntity<Claim> submitClaim(@PathVariable Long claimId) {
Claim claim = claimService.submitClaim(claimId);
return ResponseEntity.ok(claim);
}
@PostMapping("/approve")
@ApiOperation("核准理赔")
@PreAuthorize("hasRole('CLAIM_APPROVER') or hasRole('ADMIN')")
public ResponseEntity<Claim> approveClaim(@RequestBody ClaimApprovalDTO approvalDTO) {
Claim claim = claimService.approveClaim(approvalDTO);
return ResponseEntity.ok(claim);
}
@PostMapping("/{claimId}/investigate")
@ApiOperation("开始理赔调查")
@PreAuthorize("hasRole('CLAIM_INVESTIGATOR') or hasRole('ADMIN')")
public ResponseEntity<Claim> startInvestigation(@PathVariable Long claimId) {
Claim claim = claimService.startInvestigation(claimId);
return ResponseEntity.ok(claim);
}
@PostMapping("/investigation")
@ApiOperation("添加调查记录")
@PreAuthorize("hasRole('CLAIM_INVESTIGATOR') or hasRole('ADMIN')")
public ResponseEntity<Claim> addInvestigation(@RequestBody ClaimInvestigationDTO investigationDTO) {
Claim claim = claimService.addInvestigation(investigationDTO);
return ResponseEntity.ok(claim);
}
@PostMapping("/{claimId}/payment")
@ApiOperation("处理理赔付款")
@PreAuthorize("hasRole('CLAIM_PAYMENT') or hasRole('ADMIN')")
public ResponseEntity<Claim> processPayment(@PathVariable Long claimId) {
Claim claim = claimService.processPayment(claimId);
return ResponseEntity.ok(claim);
}
@GetMapping("/export")
@ApiOperation("导出理赔Excel")
@PreAuthorize("hasRole('CLAIM_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<byte[]> exportClaimsToExcel(DateRangeDTO dateRange) {
byte[] excelData = claimService.exportClaimsToExcel(dateRange);
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=claims.xlsx")
.body(excelData);
}
@PostMapping("/{claimId}/documents")
@ApiOperation("上传理赔材料")
@PreAuthorize("hasAnyRole('CLAIM', 'ADMIN')")
public ResponseEntity<ClaimDocument> uploadDocument(
@PathVariable Long claimId,
@RequestParam("file") MultipartFile file,
@RequestParam String documentType) {
ClaimDocument document = claimService.uploadDocument(claimId, file, documentType);
return ResponseEntity.ok(document);
}
@GetMapping("/statistics")
@ApiOperation("获取理赔统计")
@PreAuthorize("hasRole('CLAIM_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<ClaimStatisticsDTO> getClaimStatistics(DateRangeDTO dateRange) {
ClaimStatisticsDTO statistics = claimService.getClaimStatistics(dateRange);
return ResponseEntity.ok(statistics);
}
}
Service层
java
public interface ClaimService {
Claim createClaim(ClaimDTO claimDTO);
Claim updateClaim(Long id, ClaimDTO claimDTO);
Claim getClaimById(Long id);
Claim getClaimByNo(String claimNo);
Page<Claim> searchClaims(ClaimSearchDTO searchDTO, Pageable pageable);
List<Claim> getClaimsByPolicy(Long policyId);
Claim submitClaim(Long claimId);
Claim approveClaim(ClaimApprovalDTO approvalDTO);
Claim startInvestigation(Long claimId);
Claim addInvestigation(ClaimInvestigationDTO investigationDTO);
Claim processPayment(Long claimId);
byte[] exportClaimsToExcel(DateRangeDTO dateRange);
ClaimDocument uploadDocument(Long claimId, MultipartFile file, String documentType);
ClaimStatisticsDTO getClaimStatistics(DateRangeDTO dateRange);
}
java
@Slf4j
@Service
@Transactional
public class ClaimServiceImpl implements ClaimService {
private final ClaimRepository claimRepository;
private final PolicyRepository policyRepository;
private final ClaimDocumentRepository documentRepository;
private final ClaimBeneficiaryRepository beneficiaryRepository;
private final ClaimInvestigationRepository investigationRepository;
private final ModelMapper modelMapper;
private final ClaimNumberGenerator claimNumberGenerator;
private final FileStorageService fileStorageService;
private final RabbitTemplate rabbitTemplate;
private final AuditLogService auditLogService;
public ClaimServiceImpl(ClaimRepository claimRepository,
PolicyRepository policyRepository,
ClaimDocumentRepository documentRepository,
ClaimBeneficiaryRepository beneficiaryRepository,
ClaimInvestigationRepository investigationRepository,
ModelMapper modelMapper,
ClaimNumberGenerator claimNumberGenerator,
FileStorageService fileStorageService,
RabbitTemplate rabbitTemplate,
AuditLogService auditLogService) {
this.claimRepository = claimRepository;
this.policyRepository = policyRepository;
this.documentRepository = documentRepository;
this.beneficiaryRepository = beneficiaryRepository;
this.investigationRepository = investigationRepository;
this.modelMapper = modelMapper;
this.claimNumberGenerator = claimNumberGenerator;
this.fileStorageService = fileStorageService;
this.rabbitTemplate = rabbitTemplate;
this.auditLogService = auditLogService;
}
@Override
@CacheEvict(value = "claims", allEntries = true)
public Claim createClaim(ClaimDTO claimDTO) {
Policy policy = policyRepository.findById(claimDTO.getPolicyId())
.orElseThrow(() -> new ResourceNotFoundException("保单不存在"));
// 验证保单状态是否可以理赔
if (policy.getStatus() != PolicyStatus.ACTIVE &&
policy.getStatus() != PolicyStatus.UNDERWRITTEN) {
throw new BusinessException("当前保单状态不能申请理赔");
}
// 生成理赔单号
String claimNo = claimNumberGenerator.generateClaimNo(policy.getPolicyNo());
// 创建理赔记录
Claim claim = modelMapper.map(claimDTO, Claim.class);
claim.setClaimNo(claimNo);
claim.setPolicy(policy);
claim.setStatus(ClaimStatus.DRAFT);
claim.setCreatedAt(new Date());
claim.setUpdatedAt(new Date());
Claim savedClaim = claimRepository.save(claim);
// 保存理赔材料
saveDocuments(claimDTO, savedClaim);
// 保存理赔受益人
saveBeneficiaries(claimDTO, savedClaim);
// 记录审计日志
auditLogService.logClaimCreation(savedClaim);
return savedClaim;
}
@Override
@CacheEvict(value = "claims", key = "#id")
public Claim updateClaim(Long id, ClaimDTO claimDTO) {
Claim existingClaim = claimRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("理赔记录不存在"));
// 检查是否可以修改
if (existingClaim.getStatus() != ClaimStatus.DRAFT) {
throw new BusinessException("只有草稿状态的理赔可以修改");
}
modelMapper.map(claimDTO, existingClaim);
existingClaim.setUpdatedAt(new Date());
// 更新理赔材料
updateDocuments(claimDTO, existingClaim);
// 更新理赔受益人
updateBeneficiaries(claimDTO, existingClaim);
return claimRepository.save(existingClaim);
}
@Override
@Cacheable(value = "claims", key = "#id")
public Claim getClaimById(Long id) {
return claimRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("理赔记录不存在"));
}
@Override
@Cacheable(value = "claims", key = "#claimNo")
public Claim getClaimByNo(String claimNo) {
return claimRepository.findByClaimNo(claimNo)
.orElseThrow(() -> new ResourceNotFoundException("理赔记录不存在"));
}
@Override
public Page<Claim> searchClaims(ClaimSearchDTO searchDTO, Pageable pageable) {
return claimRepository.searchClaims(
searchDTO.getClaimNo(),
searchDTO.getPolicyNo(),
searchDTO.getStatus(),
searchDTO.getStartDate(),
searchDTO.getEndDate(),
pageable);
}
@Override
public List<Claim> getClaimsByPolicy(Long policyId) {
return claimRepository.findByPolicyId(policyId);
}
@Override
@CacheEvict(value = "claims", key = "#claimId")
public Claim submitClaim(Long claimId) {
Claim claim = claimRepository.findById(claimId)
.orElseThrow(() -> new ResourceNotFoundException("理赔记录不存在"));
// 检查是否可以提交
if (claim.getStatus() != ClaimStatus.DRAFT) {
throw new BusinessException("只有草稿状态的理赔可以提交");
}
// 验证必要材料是否齐全
validateRequiredDocuments(claim);
claim.setStatus(ClaimStatus.REPORTED);
claim.setUpdatedAt(new Date());
Claim updatedClaim = claimRepository.save(claim);
// 启动理赔工作流
startClaimWorkflow(updatedClaim);
// 记录审计日志
auditLogService.logClaimSubmission(updatedClaim);
return updatedClaim;
}
@Override
@CacheEvict(value = "claims", key = "#approvalDTO.claimId")
public Claim approveClaim(ClaimApprovalDTO approvalDTO) {
Claim claim = claimRepository.findById(approvalDTO.getClaimId())
.orElseThrow(() -> new ResourceNotFoundException("理赔记录不存在"));
// 检查是否可以核准
if (claim.getStatus() != ClaimStatus.UNDER_REVIEW &&
claim.getStatus() != ClaimStatus.INVESTIGATING) {
throw new BusinessException("当前状态不能核准理赔");
}
claim.setStatus(approvalDTO.getDecision());
claim.setApprovedAmount(approvalDTO.getApprovedAmount());
claim.setApprovalDate(new Date());
claim.setUpdatedAt(new Date());
Claim updatedClaim = claimRepository.save(claim);
// 如果是核准状态,准备付款
if (approvalDTO.getDecision() == ClaimStatus.APPROVED ||
approvalDTO.getDecision() == ClaimStatus.PARTIALLY_APPROVED) {
rabbitTemplate.convertAndSend("claim.payment.queue", updatedClaim.getId());
}
// 记录审计日志
auditLogService.logClaimApproval(updatedClaim);
return updatedClaim;
}
@Override
@CacheEvict(value = "claims", key = "#claimId")
public Claim startInvestigation(Long claimId) {
Claim claim = claimRepository.findById(claimId)
.orElseThrow(() -> new ResourceNotFoundException("理赔记录不存在"));
// 检查是否可以开始调查
if (claim.getStatus() != ClaimStatus.REPORTED &&
claim.getStatus() != ClaimStatus.UNDER_REVIEW) {
throw new BusinessException("当前状态不能开始调查");
}
claim.setStatus(ClaimStatus.INVESTIGATING);
claim.setUpdatedAt(new Date());
Claim updatedClaim = claimRepository.save(claim);
// 发送调查通知
rabbitTemplate.convertAndSend("claim.investigation.queue", updatedClaim.getId());
return updatedClaim;
}
@Override
@CacheEvict(value = "claims", key = "#investigationDTO.claimId")
public Claim addInvestigation(ClaimInvestigationDTO investigationDTO) {
Claim claim = claimRepository.findById(investigationDTO.getClaimId())
.orElseThrow(() -> new ResourceNotFoundException("理赔记录不存在"));
// 检查是否可以添加调查
if (claim.getStatus() != ClaimStatus.INVESTIGATING) {
throw new BusinessException("当前状态不能添加调查记录");
}
ClaimInvestigation investigation = modelMapper.map(investigationDTO, ClaimInvestigation.class);
investigation.setClaim(claim);
investigation.setCreatedAt(new Date());
investigationRepository.save(investigation);
// 调查完成后自动转回审核状态
claim.setStatus(ClaimStatus.UNDER_REVIEW);
claim.setUpdatedAt(new Date());
Claim updatedClaim = claimRepository.save(claim);
return updatedClaim;
}
@Override
@CacheEvict(value = "claims", key = "#claimId")
public Claim processPayment(Long claimId) {
Claim claim = claimRepository.findById(claimId)
.orElseThrow(() -> new ResourceNotFoundException("理赔记录不存在"));
// 检查是否可以付款
if (claim.getStatus() != ClaimStatus.APPROVED &&
claim.getStatus() != ClaimStatus.PARTIALLY_APPROVED) {
throw new BusinessException("只有核准状态的理赔可以付款");
}
// 这里应该有实际的付款逻辑
// 简化版示例:假设付款成功
claim.setStatus(ClaimStatus.PAID);
claim.setUpdatedAt(new Date());
Claim updatedClaim = claimRepository.save(claim);
// 更新保单状态
Policy policy = claim.getPolicy();
policy.setStatus(PolicyStatus.CLAIMED);
policyRepository.save(policy);
// 记录审计日志
auditLogService.logClaimPayment(updatedClaim);
return updatedClaim;
}
@Override
public byte[] exportClaimsToExcel(DateRangeDTO dateRange) {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
Sheet sheet = workbook.createSheet("理赔列表");
// 创建标题行
Row headerRow = sheet.createRow(0);
String[] headers = {"理赔单号", "保单号", "状态", "事故日期", "报案日期", "索赔金额", "核定金额", "核定日期"};
for (int i = 0; i < headers.length; i++) {
headerRow.createCell(i).setCellValue(headers[i]);
}
// 填充数据
List<Claim> claims = claimRepository.findAllByApprovalDateBetween(
dateRange.getStartDate(), dateRange.getEndDate());
int rowNum = 1;
for (Claim claim : claims) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(claim.getClaimNo());
row.createCell(1).setCellValue(claim.getPolicy().getPolicyNo());
row.createCell(2).setCellValue(claim.getStatus().name());
row.createCell(3).setCellValue(claim.getIncidentDate().toString());
row.createCell(4).setCellValue(claim.getReportDate().toString());
row.createCell(5).setCellValue(claim.getClaimedAmount().doubleValue());
row.createCell(6).setCellValue(
claim.getApprovedAmount() != null ? claim.getApprovedAmount().doubleValue() : 0);
row.createCell(7).setCellValue(
claim.getApprovalDate() != null ? claim.getApprovalDate().toString() : "");
}
workbook.write(out);
return out.toByteArray();
} catch (IOException e) {
log.error("导出理赔Excel失败", e);
throw new BusinessException("导出理赔Excel失败");
}
}
@Override
public ClaimDocument uploadDocument(Long claimId, MultipartFile file, String documentType) {
Claim claim = claimRepository.findById(claimId)
.orElseThrow(() -> new ResourceNotFoundException("理赔记录不存在"));
// 检查是否可以上传材料
if (claim.getStatus() == ClaimStatus.PAID || claim.getStatus() == ClaimStatus.CLOSED) {
throw new BusinessException("当前状态不能上传材料");
}
// 存储文件
String filePath = fileStorageService.storeFile(file);
// 创建文档记录
ClaimDocument document = new ClaimDocument();
document.setClaim(claim);
document.setDocumentType(documentType);
document.setFileName(file.getOriginalFilename());
document.setFilePath(filePath);
document.setFileSize(file.getSize());
document.setFileType(file.getContentType());
return documentRepository.save(document);
}
@Override
public ClaimStatisticsDTO getClaimStatistics(DateRangeDTO dateRange) {
ClaimStatisticsDTO statistics = new ClaimStatisticsDTO();
// 获取已赔付理赔数量
statistics.setPaidClaimCount(
claimRepository.countPaidClaimsBetween(dateRange.getStartDate(), dateRange.getEndDate()));
// 获取已赔付总金额
BigDecimal paidAmount = claimRepository.sumPaidAmountBetween(
dateRange.getStartDate(), dateRange.getEndDate());
statistics.setTotalPaidAmount(paidAmount != null ? paidAmount : BigDecimal.ZERO);
// 其他统计指标...
return statistics;
}
private void saveDocuments(ClaimDTO claimDTO, Claim claim) {
for (ClaimDTO.ClaimDocumentDTO documentDTO : claimDTO.getDocuments()) {
ClaimDocument document = modelMapper.map(documentDTO, ClaimDocument.class);
document.setClaim(claim);
documentRepository.save(document);
}
}
private void updateDocuments(ClaimDTO claimDTO, Claim claim) {
// 删除旧文档记录(实际文件不删除)
documentRepository.deleteByClaimId(claim.getId());
// 添加新文档记录
saveDocuments(claimDTO, claim);
}
private void saveBeneficiaries(ClaimDTO claimDTO, Claim claim) {
for (ClaimDTO.ClaimBeneficiaryDTO beneficiaryDTO : claimDTO.getBeneficiaries()) {
ClaimBeneficiary beneficiary = modelMapper.map(beneficiaryDTO, ClaimBeneficiary.class);
beneficiary.setClaim(claim);
beneficiaryRepository.save(beneficiary);
}
}
private void updateBeneficiaries(ClaimDTO claimDTO, Claim claim) {
// 删除旧受益人
beneficiaryRepository.deleteByClaimId(claim.getId());
// 添加新受益人
saveBeneficiaries(claimDTO, claim);
}
private void validateRequiredDocuments(Claim claim) {
// 这里应该有业务规则验证必要材料是否齐全
// 简化版示例:至少需要一个材料
if (documentRepository.countByClaimId(claim.getId()) == 0) {
throw new BusinessException("必须上传至少一个理赔材料");
}
}
private void startClaimWorkflow(Claim claim) {
// 这里应该有启动Activiti工作流的逻辑
// 简化版示例:发送到消息队列
rabbitTemplate.convertAndSend("claim.process.queue", claim.getId());
}
}
Repository层
java
ublic interface ClaimRepository extends JpaRepository<Claim, Long> {
Claim findByClaimNo(String claimNo);
List<Claim> findByPolicyId(Long policyId);
List<Claim> findByStatus(ClaimStatus status);
@Query("SELECT c FROM Claim c WHERE " +
"(:claimNo IS NULL OR c.claimNo LIKE %:claimNo%) AND " +
"(:policyNo IS NULL OR c.policy.policyNo LIKE %:policyNo%) AND " +
"(:status IS NULL OR c.status = :status) AND " +
"(:startDate IS NULL OR c.incidentDate >= :startDate) AND " +
"(:endDate IS NULL OR c.incidentDate <= :endDate)")
Page<Claim> searchClaims(@Param("claimNo") String claimNo,
@Param("policyNo") String policyNo,
@Param("status") ClaimStatus status,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate,
Pageable pageable);
@Query("SELECT COUNT(c) FROM Claim c WHERE c.status = 'PAID' AND c.approvalDate BETWEEN :start AND :end")
Long countPaidClaimsBetween(@Param("start") Date start, @Param("end") Date end);
@Query("SELECT SUM(c.approvedAmount) FROM Claim c WHERE c.status = 'PAID' AND c.approvalDate BETWEEN :start AND :end")
BigDecimal sumPaidAmountBetween(@Param("start") Date start, @Param("end") Date end);
}
java
public interface ClaimDocumentRepository extends JpaRepository<ClaimDocument, Long> {
List<ClaimDocument> findByClaimId(Long claimId);
List<ClaimDocument> findByClaimIdAndDocumentType(Long claimId, String documentType);
}
6. 收付费管理
保费收取与对账
佣金计算与发放
赔款支付
财务报表
yml
# 支付网关配置
payment:
alipay:
app-id: your-app-id
merchant-private-key: your-private-key
alipay-public-key: your-public-key
gateway-url: https://openapi.alipay.com/gateway.do
wechat:
app-id: your-app-id
mch-id: your-mch-id
api-key: your-api-key
cert-path: classpath:cert/apiclient_cert.p12
# 缓存配置
cache:
payment:
ttl: 86400 # 24小时
java
@Component
@Slf4j
public class ReconciliationProcessor {
private final PaymentRepository paymentRepository;
public ReconciliationProcessor(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
public List<Payment> processReconciliationFile(String bankName, MultipartFile file) {
try {
String extension = FilenameUtils.getExtension(file.getOriginalFilename());
if ("xls".equalsIgnoreCase(extension) || "xlsx".equalsIgnoreCase(extension)) {
return processExcelReconciliation(bankName, file);
} else if ("txt".equalsIgnoreCase(extension) || "csv".equalsIgnoreCase(extension)) {
return processTextReconciliation(bankName, file);
} else {
throw new IllegalArgumentException("不支持的文件类型");
}
} catch (IOException e) {
log.error("对账文件处理失败", e);
throw new RuntimeException("对账文件处理失败", e);
}
}
private List<Payment> processExcelReconciliation(String bankName, MultipartFile file) throws IOException {
List<Payment> matchedPayments = new ArrayList<>();
Workbook workbook = WorkbookFactory.create(file.getInputStream());
Sheet sheet = workbook.getSheetAt(0);
for (Row row : sheet) {
if (row.getRowNum() == 0) continue; // 跳过标题行
// 解析银行交易记录 (根据实际银行文件格式调整)
String transactionId = row.getCell(0).getStringCellValue();
BigDecimal amount = BigDecimal.valueOf(row.getCell(1).getNumericCellValue());
Date transactionDate = row.getCell(2).getDateCellValue();
// 查找匹配的支付记录
Payment payment = paymentRepository.findByTransactionId(transactionId);
if (payment != null && payment.getAmount().compareTo(amount) == 0) {
matchedPayments.add(payment);
}
}
workbook.close();
return matchedPayments;
}
private List<Payment> processTextReconciliation(String bankName, MultipartFile file) throws IOException {
// 处理文本格式的对账文件
// 实现逻辑...
return new ArrayList<>();
}
}
java
@Component
public class PaymentNumberGenerator {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
private static int sequence = 0;
private static final int MAX_SEQUENCE = 9999;
public synchronized String generatePaymentNo(String policyNo, PaymentType type) {
String prefix;
switch (type) {
case PREMIUM:
prefix = "P";
break;
case REFUND:
prefix = "R";
break;
case CLAIM:
prefix = "C";
break;
default:
prefix = "O";
}
String datePart = dateFormat.format(new Date());
if (sequence >= MAX_SEQUENCE) {
sequence = 0;
}
sequence++;
String sequencePart = String.format("%04d", sequence);
return prefix + policyNo.substring(3, 7) + datePart + sequencePart;
}
}
Controller层
java
@RestController
@RequestMapping("/api/payments")
@Api(tags = "收付费管理")
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostMapping
@ApiOperation("创建支付记录")
@PreAuthorize("hasRole('FINANCE') or hasRole('ADMIN')")
public ResponseEntity<Payment> createPayment(@RequestBody PaymentDTO paymentDTO) {
Payment payment = paymentService.createPayment(paymentDTO);
return ResponseEntity.ok(payment);
}
@PutMapping("/{id}")
@ApiOperation("更新支付记录")
@PreAuthorize("hasRole('FINANCE') or hasRole('ADMIN')")
public ResponseEntity<Payment> updatePayment(@PathVariable Long id,
@RequestBody PaymentDTO paymentDTO) {
Payment payment = paymentService.updatePayment(id, paymentDTO);
return ResponseEntity.ok(payment);
}
@GetMapping("/{id}")
@ApiOperation("获取支付记录详情")
@PreAuthorize("hasAnyRole('FINANCE', 'AGENT', 'ADMIN')")
public ResponseEntity<Payment> getPaymentById(@PathVariable Long id) {
Payment payment = paymentService.getPaymentById(id);
return ResponseEntity.ok(payment);
}
@GetMapping("/number/{paymentNo}")
@ApiOperation("通过支付流水号获取支付记录")
@PreAuthorize("hasAnyRole('FINANCE', 'AGENT', 'ADMIN')")
public ResponseEntity<Payment> getPaymentByNo(@PathVariable String paymentNo) {
Payment payment = paymentService.getPaymentByNo(paymentNo);
return ResponseEntity.ok(payment);
}
@GetMapping("/search")
@ApiOperation("搜索支付记录")
@PreAuthorize("hasAnyRole('FINANCE', 'AGENT', 'ADMIN')")
public ResponseEntity<Page<Payment>> searchPayments(PaymentSearchDTO searchDTO,
Pageable pageable) {
Page<Payment> payments = paymentService.searchPayments(searchDTO, pageable);
return ResponseEntity.ok(payments);
}
@GetMapping("/policy/{policyId}")
@ApiOperation("获取保单的支付记录")
@PreAuthorize("hasAnyRole('FINANCE', 'AGENT', 'ADMIN')")
public ResponseEntity<List<Payment>> getPaymentsByPolicy(@PathVariable Long policyId) {
List<Payment> payments = paymentService.getPaymentsByPolicy(policyId);
return ResponseEntity.ok(payments);
}
@PostMapping("/initiate")
@ApiOperation("发起支付")
@PreAuthorize("hasAnyRole('FINANCE', 'AGENT', 'ADMIN')")
public ResponseEntity<Payment> initiatePayment(@RequestBody PaymentRequestDTO requestDTO) {
Payment payment = paymentService.initiatePayment(requestDTO);
return ResponseEntity.ok(payment);
}
@PostMapping("/callback")
@ApiOperation("支付回调处理")
public ResponseEntity<Payment> processPaymentCallback(@RequestBody PaymentCallbackDTO callbackDTO) {
Payment payment = paymentService.processPaymentCallback(callbackDTO);
return ResponseEntity.ok(payment);
}
@PutMapping("/{paymentId}/cancel")
@ApiOperation("取消支付")
@PreAuthorize("hasRole('FINANCE') or hasRole('ADMIN')")
public ResponseEntity<Payment> cancelPayment(@PathVariable Long paymentId) {
Payment payment = paymentService.cancelPayment(paymentId);
return ResponseEntity.ok(payment);
}
@PostMapping("/reconciliation/{bankName}")
@ApiOperation("银行对账")
@PreAuthorize("hasRole('FINANCE_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<List<Payment>> processReconciliation(
@PathVariable String bankName,
@RequestParam("file") MultipartFile file) {
List<Payment> payments = paymentService.processReconciliation(bankName, file);
return ResponseEntity.ok(payments);
}
@GetMapping("/statistics")
@ApiOperation("获取支付统计")
@PreAuthorize("hasRole('FINANCE_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<PaymentStatisticsDTO> getPaymentStatistics(DateRangeDTO dateRange) {
PaymentStatisticsDTO statistics = paymentService.getPaymentStatistics(dateRange);
return ResponseEntity.ok(statistics);
}
@GetMapping("/report")
@ApiOperation("生成支付报表")
@PreAuthorize("hasRole('FINANCE_MANAGER') or hasRole('ADMIN')")
public ResponseEntity<byte[]> generatePaymentReport(DateRangeDTO dateRange) {
byte[] report = paymentService.generatePaymentReport(dateRange);
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=payment_report.pdf")
.body(report);
}
}
Service层
java
public interface PaymentService {
Payment createPayment(PaymentDTO paymentDTO);
Payment updatePayment(Long id, PaymentDTO paymentDTO);
Payment getPaymentById(Long id);
Payment getPaymentByNo(String paymentNo);
Page<Payment> searchPayments(PaymentSearchDTO searchDTO, Pageable pageable);
List<Payment> getPaymentsByPolicy(Long policyId);
Payment initiatePayment(PaymentRequestDTO requestDTO);
Payment processPaymentCallback(PaymentCallbackDTO callbackDTO);
Payment cancelPayment(Long paymentId);
List<Payment> processReconciliation(String bankName, MultipartFile file);
PaymentStatisticsDTO getPaymentStatistics(DateRangeDTO dateRange);
byte[] generatePaymentReport(DateRangeDTO dateRange);
}
java
@Slf4j
@Service
@Transactional
public class PaymentServiceImpl implements PaymentService {
private final PaymentRepository paymentRepository;
private final PolicyRepository policyRepository;
private final CommissionRepository commissionRepository;
private final AgentRepository agentRepository;
private final ModelMapper modelMapper;
private final PaymentNumberGenerator paymentNumberGenerator;
private final ReconciliationProcessor reconciliationProcessor;
private final RabbitTemplate rabbitTemplate;
private final AuditLogService auditLogService;
public PaymentServiceImpl(PaymentRepository paymentRepository,
PolicyRepository policyRepository,
CommissionRepository commissionRepository,
AgentRepository agentRepository,
ModelMapper modelMapper,
PaymentNumberGenerator paymentNumberGenerator,
ReconciliationProcessor reconciliationProcessor,
RabbitTemplate rabbitTemplate,
AuditLogService auditLogService) {
this.paymentRepository = paymentRepository;
this.policyRepository = policyRepository;
this.commissionRepository = commissionRepository;
this.agentRepository = agentRepository;
this.modelMapper = modelMapper;
this.paymentNumberGenerator = paymentNumberGenerator;
this.reconciliationProcessor = reconciliationProcessor;
this.rabbitTemplate = rabbitTemplate;
this.auditLogService = auditLogService;
}
@Override
@CacheEvict(value = "payments", allEntries = true)
public Payment createPayment(PaymentDTO paymentDTO) {
Policy policy = policyRepository.findById(paymentDTO.getPolicyId())
.orElseThrow(() -> new ResourceNotFoundException("保单不存在"));
// 生成支付流水号
String paymentNo = paymentNumberGenerator.generatePaymentNo(policy.getPolicyNo(), paymentDTO.getType());
// 创建支付记录
Payment payment = modelMapper.map(paymentDTO, Payment.class);
payment.setPaymentNo(paymentNo);
payment.setPolicy(policy);
payment.setCreatedAt(new Date());
payment.setUpdatedAt(new Date());
Payment savedPayment = paymentRepository.save(payment);
// 如果是保费支付且状态为已完成,计算佣金
if (paymentDTO.getType() == PaymentType.PREMIUM &&
paymentDTO.getStatus() == PaymentStatus.COMPLETED) {
calculateCommissions(savedPayment);
}
// 记录审计日志
auditLogService.logPaymentCreation(savedPayment);
return savedPayment;
}
@Override
@CacheEvict(value = "payments", key = "#id")
public Payment updatePayment(Long id, PaymentDTO paymentDTO) {
Payment existingPayment = paymentRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("支付记录不存在"));
// 检查是否可以修改
if (existingPayment.getStatus() != PaymentStatus.PENDING &&
existingPayment.getStatus() != PaymentStatus.PROCESSING) {
throw new BusinessException("只有待支付或处理中的支付记录可以修改");
}
modelMapper.map(paymentDTO, existingPayment);
existingPayment.setUpdatedAt(new Date());
Payment updatedPayment = paymentRepository.save(existingPayment);
// 如果状态变更为已完成,计算佣金
if (paymentDTO.getStatus() == PaymentStatus.COMPLETED &&
updatedPayment.getType() == PaymentType.PREMIUM) {
calculateCommissions(updatedPayment);
}
return updatedPayment;
}
@Override
@Cacheable(value = "payments", key = "#id")
public Payment getPaymentById(Long id) {
return paymentRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("支付记录不存在"));
}
@Override
@Cacheable(value = "payments", key = "#paymentNo")
public Payment getPaymentByNo(String paymentNo) {
return paymentRepository.findByPaymentNo(paymentNo)
.orElseThrow(() -> new ResourceNotFoundException("支付记录不存在"));
}
@Override
public Page<Payment> searchPayments(PaymentSearchDTO searchDTO, Pageable pageable) {
return paymentRepository.searchPayments(
searchDTO.getPaymentNo(),
searchDTO.getPolicyNo(),
searchDTO.getType(),
searchDTO.getStatus(),
searchDTO.getStartDate(),
searchDTO.getEndDate(),
pageable);
}
@Override
public List<Payment> getPaymentsByPolicy(Long policyId) {
return paymentRepository.findByPolicyId(policyId);
}
@Override
@Transactional
public Payment initiatePayment(PaymentRequestDTO requestDTO) {
Policy policy = policyRepository.findById(requestDTO.getPolicyId())
.orElseThrow(() -> new ResourceNotFoundException("保单不存在"));
// 生成支付流水号
String paymentNo = paymentNumberGenerator.generatePaymentNo(policy.getPolicyNo(), PaymentType.PREMIUM);
// 创建支付记录
Payment payment = new Payment();
payment.setPaymentNo(paymentNo);
payment.setPolicy(policy);
payment.setType(PaymentType.PREMIUM);
payment.setStatus(PaymentStatus.PENDING);
payment.setAmount(requestDTO.getAmount());
payment.setPaymentDate(new Date());
payment.setPaymentMethod(requestDTO.getPaymentMethod());
payment.setCreatedAt(new Date());
payment.setUpdatedAt(new Date());
Payment savedPayment = paymentRepository.save(payment);
// 调用支付网关
initiatePaymentGateway(savedPayment);
// 记录审计日志
auditLogService.logPaymentInitiation(savedPayment);
return savedPayment;
}
@Override
@CacheEvict(value = "payments", key = "#callbackDTO.paymentNo")
public Payment processPaymentCallback(PaymentCallbackDTO callbackDTO) {
Payment payment = paymentRepository.findByPaymentNo(callbackDTO.getPaymentNo())
.orElseThrow(() -> new ResourceNotFoundException("支付记录不存在"));
payment.setStatus(callbackDTO.getStatus());
payment.setTransactionId(callbackDTO.getTransactionId());
payment.setRemark(callbackDTO.getRemark());
payment.setUpdatedAt(new Date());
if (callbackDTO.getStatus() == PaymentStatus.COMPLETED) {
payment.setPaymentDate(new Date());
// 如果是保费支付,计算佣金
if (payment.getType() == PaymentType.PREMIUM) {
calculateCommissions(payment);
}
// 更新保单状态
updatePolicyAfterPayment(payment);
}
Payment updatedPayment = paymentRepository.save(payment);
// 记录审计日志
auditLogService.logPaymentCompletion(updatedPayment);
return updatedPayment;
}
@Override
@CacheEvict(value = "payments", key = "#paymentId")
public Payment cancelPayment(Long paymentId) {
Payment payment = paymentRepository.findById(paymentId)
.orElseThrow(() -> new ResourceNotFoundException("支付记录不存在"));
// 检查是否可以取消
if (payment.getStatus() != PaymentStatus.PENDING &&
payment.getStatus() != PaymentStatus.PROCESSING) {
throw new BusinessException("只有待支付或处理中的支付记录可以取消");
}
payment.setStatus(PaymentStatus.CANCELLED);
payment.setUpdatedAt(new Date());
Payment updatedPayment = paymentRepository.save(payment);
// 取消关联的佣金
cancelCommissions(updatedPayment);
// 记录审计日志
auditLogService.logPaymentCancellation(updatedPayment);
return updatedPayment;
}
@Override
public List<Payment> processReconciliation(String bankName, MultipartFile file) {
// 处理银行对账文件
List<Payment> matchedPayments = reconciliationProcessor.processReconciliationFile(bankName, file);
// 更新匹配的支付记录
matchedPayments.forEach(payment -> {
payment.setStatus(PaymentStatus.COMPLETED);
payment.setUpdatedAt(new Date());
paymentRepository.save(payment);
// 如果是保费支付,计算佣金
if (payment.getType() == PaymentType.PREMIUM) {
calculateCommissions(payment);
}
});
return matchedPayments;
}
@Override
public PaymentStatisticsDTO getPaymentStatistics(DateRangeDTO dateRange) {
PaymentStatisticsDTO statistics = new PaymentStatisticsDTO();
// 获取保费收入总额
BigDecimal premiumIncome = paymentRepository.sumPremiumPaymentsBetween(
dateRange.getStartDate(), dateRange.getEndDate());
statistics.setPremiumIncome(premiumIncome != null ? premiumIncome : BigDecimal.ZERO);
// 获取理赔支出总额
BigDecimal claimPayments = paymentRepository.sumClaimPaymentsBetween(
dateRange.getStartDate(), dateRange.getEndDate());
statistics.setClaimPayments(claimPayments != null ? claimPayments : BigDecimal.ZERO);
// 获取佣金支出总额
BigDecimal commissionPayments = commissionRepository.sumPaidCommissionsBetween(
dateRange.getStartDate(), dateRange.getEndDate());
statistics.setCommissionPayments(commissionPayments != null ? commissionPayments : BigDecimal.ZERO);
return statistics;
}
@Override
public byte[] generatePaymentReport(DateRangeDTO dateRange) {
// 实际项目中应使用JasperReports等工具生成PDF报表
// 这里简化为返回空数组
return new byte[0];
}
private void calculateCommissions(Payment payment) {
Policy policy = payment.getPolicy();
// 获取保单的代理人
Agent agent = policy.getAgent();
if (agent == null) {
return;
}
// 计算佣金 (简化版示例:固定佣金比例)
BigDecimal commissionRate = BigDecimal.valueOf(0.15); // 15%佣金
BigDecimal commissionAmount = payment.getAmount().multiply(commissionRate);
// 创建佣金记录
Commission commission = new Commission();
commission.setPayment(payment);
commission.setAgent(agent);
commission.setAmount(commissionAmount);
commission.setRate(commissionRate);
commission.setCalculatedDate(new Date());
commission.setStatus(CommissionStatus.PENDING);
commission.setCreatedAt(new Date());
commissionRepository.save(commission);
// 发送佣金支付通知
rabbitTemplate.convertAndSend("commission.payment.queue", commission.getId());
}
private void cancelCommissions(Payment payment) {
List<Commission> commissions = commissionRepository.findByPaymentId(payment.getId());
commissions.forEach(commission -> {
commission.setStatus(CommissionStatus.CANCELLED);
commissionRepository.save(commission);
});
}
private void updatePolicyAfterPayment(Payment payment) {
Policy policy = payment.getPolicy();
if (payment.getType() == PaymentType.PREMIUM) {
// 保费支付成功后更新保单状态
if (policy.getStatus() == PolicyStatus.PENDING_PAYMENT) {
policy.setStatus(PolicyStatus.ACTIVE);
policyRepository.save(policy);
// 发送保单激活通知
rabbitTemplate.convertAndSend("policy.activated.queue", policy.getId());
}
}
}
private void initiatePaymentGateway(Payment payment) {
// 这里应该有调用实际支付网关的逻辑
// 简化版示例:发送到消息队列异步处理
rabbitTemplate.convertAndSend("payment.process.queue", payment.getId());
}
@Scheduled(cron = "0 0 9 * * ?") // 每天上午9点执行
public void processDailyReconciliation() {
// 自动下载银行对账文件并处理
log.info("开始执行每日自动对账...");
// 实现逻辑...
}
}
Repository层
java
@Slf4j
@Service
@Transactional
public class PaymentServiceImpl implements PaymentService {
private final PaymentRepository paymentRepository;
private final PolicyRepository policyRepository;
private final CommissionRepository commissionRepository;
private final AgentRepository agentRepository;
private final ModelMapper modelMapper;
private final PaymentNumberGenerator paymentNumberGenerator;
private final ReconciliationProcessor reconciliationProcessor;
private final RabbitTemplate rabbitTemplate;
private final AuditLogService auditLogService;
public PaymentServiceImpl(PaymentRepository paymentRepository,
PolicyRepository policyRepository,
CommissionRepository commissionRepository,
AgentRepository agentRepository,
ModelMapper modelMapper,
PaymentNumberGenerator paymentNumberGenerator,
ReconciliationProcessor reconciliationProcessor,
RabbitTemplate rabbitTemplate,
AuditLogService auditLogService) {
this.paymentRepository = paymentRepository;
this.policyRepository = policyRepository;
this.commissionRepository = commissionRepository;
this.agentRepository = agentRepository;
this.modelMapper = modelMapper;
this.paymentNumberGenerator = paymentNumberGenerator;
this.reconciliationProcessor = reconciliationProcessor;
this.rabbitTemplate = rabbitTemplate;
this.auditLogService = auditLogService;
}
@Override
@CacheEvict(value = "payments", allEntries = true)
public Payment createPayment(PaymentDTO paymentDTO) {
Policy policy = policyRepository.findById(paymentDTO.getPolicyId())
.orElseThrow(() -> new ResourceNotFoundException("保单不存在"));
// 生成支付流水号
String paymentNo = paymentNumberGenerator.generatePaymentNo(policy.getPolicyNo(), paymentDTO.getType());
// 创建支付记录
Payment payment = modelMapper.map(paymentDTO, Payment.class);
payment.setPaymentNo(paymentNo);
payment.setPolicy(policy);
payment.setCreatedAt(new Date());
payment.setUpdatedAt(new Date());
Payment savedPayment = paymentRepository.save(payment);
// 如果是保费支付且状态为已完成,计算佣金
if (paymentDTO.getType() == PaymentType.PREMIUM &&
paymentDTO.getStatus() == PaymentStatus.COMPLETED) {
calculateCommissions(savedPayment);
}
// 记录审计日志
auditLogService.logPaymentCreation(savedPayment);
return savedPayment;
}
@Override
@CacheEvict(value = "payments", key = "#id")
public Payment updatePayment(Long id, PaymentDTO paymentDTO) {
Payment existingPayment = paymentRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("支付记录不存在"));
// 检查是否可以修改
if (existingPayment.getStatus() != PaymentStatus.PENDING &&
existingPayment.getStatus() != PaymentStatus.PROCESSING) {
throw new BusinessException("只有待支付或处理中的支付记录可以修改");
}
modelMapper.map(paymentDTO, existingPayment);
existingPayment.setUpdatedAt(new Date());
Payment updatedPayment = paymentRepository.save(existingPayment);
// 如果状态变更为已完成,计算佣金
if (paymentDTO.getStatus() == PaymentStatus.COMPLETED &&
updatedPayment.getType() == PaymentType.PREMIUM) {
calculateCommissions(updatedPayment);
}
return updatedPayment;
}
@Override
@Cacheable(value = "payments", key = "#id")
public Payment getPaymentById(Long id) {
return paymentRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("支付记录不存在"));
}
@Override
@Cacheable(value = "payments", key = "#paymentNo")
public Payment getPaymentByNo(String paymentNo) {
return paymentRepository.findByPaymentNo(paymentNo)
.orElseThrow(() -> new ResourceNotFoundException("支付记录不存在"));
}
@Override
public Page<Payment> searchPayments(PaymentSearchDTO searchDTO, Pageable pageable) {
return paymentRepository.searchPayments(
searchDTO.getPaymentNo(),
searchDTO.getPolicyNo(),
searchDTO.getType(),
searchDTO.getStatus(),
searchDTO.getStartDate(),
searchDTO.getEndDate(),
pageable);
}
@Override
public List<Payment> getPaymentsByPolicy(Long policyId) {
return paymentRepository.findByPolicyId(policyId);
}
@Override
@Transactional
public Payment initiatePayment(PaymentRequestDTO requestDTO) {
Policy policy = policyRepository.findById(requestDTO.getPolicyId())
.orElseThrow(() -> new ResourceNotFoundException("保单不存在"));
// 生成支付流水号
String paymentNo = paymentNumberGenerator.generatePaymentNo(policy.getPolicyNo(), PaymentType.PREMIUM);
// 创建支付记录
Payment payment = new Payment();
payment.setPaymentNo(paymentNo);
payment.setPolicy(policy);
payment.setType(PaymentType.PREMIUM);
payment.setStatus(PaymentStatus.PENDING);
payment.setAmount(requestDTO.getAmount());
payment.setPaymentDate(new Date());
payment.setPaymentMethod(requestDTO.getPaymentMethod());
payment.setCreatedAt(new Date());
payment.setUpdatedAt(new Date());
Payment savedPayment = paymentRepository.save(payment);
// 调用支付网关
initiatePaymentGateway(savedPayment);
// 记录审计日志
auditLogService.logPaymentInitiation(savedPayment);
return savedPayment;
}
@Override
@CacheEvict(value = "payments", key = "#callbackDTO.paymentNo")
public Payment processPaymentCallback(PaymentCallbackDTO callbackDTO) {
Payment payment = paymentRepository.findByPaymentNo(callbackDTO.getPaymentNo())
.orElseThrow(() -> new ResourceNotFoundException("支付记录不存在"));
payment.setStatus(callbackDTO.getStatus());
payment.setTransactionId(callbackDTO.getTransactionId());
payment.setRemark(callbackDTO.getRemark());
payment.setUpdatedAt(new Date());
if (callbackDTO.getStatus() == PaymentStatus.COMPLETED) {
payment.setPaymentDate(new Date());
// 如果是保费支付,计算佣金
if (payment.getType() == PaymentType.PREMIUM) {
calculateCommissions(payment);
}
// 更新保单状态
updatePolicyAfterPayment(payment);
}
Payment updatedPayment = paymentRepository.save(payment);
// 记录审计日志
auditLogService.logPaymentCompletion(updatedPayment);
return updatedPayment;
}
@Override
@CacheEvict(value = "payments", key = "#paymentId")
public Payment cancelPayment(Long paymentId) {
Payment payment = paymentRepository.findById(paymentId)
.orElseThrow(() -> new ResourceNotFoundException("支付记录不存在"));
// 检查是否可以取消
if (payment.getStatus() != PaymentStatus.PENDING &&
payment.getStatus() != PaymentStatus.PROCESSING) {
throw new BusinessException("只有待支付或处理中的支付记录可以取消");
}
payment.setStatus(PaymentStatus.CANCELLED);
payment.setUpdatedAt(new Date());
Payment updatedPayment = paymentRepository.save(payment);
// 取消关联的佣金
cancelCommissions(updatedPayment);
// 记录审计日志
auditLogService.logPaymentCancellation(updatedPayment);
return updatedPayment;
}
@Override
public List<Payment> processReconciliation(String bankName, MultipartFile file) {
// 处理银行对账文件
List<Payment> matchedPayments = reconciliationProcessor.processReconciliationFile(bankName, file);
// 更新匹配的支付记录
matchedPayments.forEach(payment -> {
payment.setStatus(PaymentStatus.COMPLETED);
payment.setUpdatedAt(new Date());
paymentRepository.save(payment);
// 如果是保费支付,计算佣金
if (payment.getType() == PaymentType.PREMIUM) {
calculateCommissions(payment);
}
});
return matchedPayments;
}
@Override
public PaymentStatisticsDTO getPaymentStatistics(DateRangeDTO dateRange) {
PaymentStatisticsDTO statistics = new PaymentStatisticsDTO();
// 获取保费收入总额
BigDecimal premiumIncome = paymentRepository.sumPremiumPaymentsBetween(
dateRange.getStartDate(), dateRange.getEndDate());
statistics.setPremiumIncome(premiumIncome != null ? premiumIncome : BigDecimal.ZERO);
// 获取理赔支出总额
BigDecimal claimPayments = paymentRepository.sumClaimPaymentsBetween(
dateRange.getStartDate(), dateRange.getEndDate());
statistics.setClaimPayments(claimPayments != null ? claimPayments : BigDecimal.ZERO);
// 获取佣金支出总额
BigDecimal commissionPayments = commissionRepository.sumPaidCommissionsBetween(
dateRange.getStartDate(), dateRange.getEndDate());
statistics.setCommissionPayments(commissionPayments != null ? commissionPayments : BigDecimal.ZERO);
return statistics;
}
@Override
public byte[] generatePaymentReport(DateRangeDTO dateRange) {
// 实际项目中应使用JasperReports等工具生成PDF报表
// 这里简化为返回空数组
return new byte[0];
}
private void calculateCommissions(Payment payment) {
Policy policy = payment.getPolicy();
// 获取保单的代理人
Agent agent = policy.getAgent();
if (agent == null) {
return;
}
// 计算佣金 (简化版示例:固定佣金比例)
BigDecimal commissionRate = BigDecimal.valueOf(0.15); // 15%佣金
BigDecimal commissionAmount = payment.getAmount().multiply(commissionRate);
// 创建佣金记录
Commission commission = new Commission();
commission.setPayment(payment);
commission.setAgent(agent);
commission.setAmount(commissionAmount);
commission.setRate(commissionRate);
commission.setCalculatedDate(new Date());
commission.setStatus(CommissionStatus.PENDING);
commission.setCreatedAt(new Date());
commissionRepository.save(commission);
// 发送佣金支付通知
rabbitTemplate.convertAndSend("commission.payment.queue", commission.getId());
}
private void cancelCommissions(Payment payment) {
List<Commission> commissions = commissionRepository.findByPaymentId(payment.getId());
commissions.forEach(commission -> {
commission.setStatus(CommissionStatus.CANCELLED);
commissionRepository.save(commission);
});
}
private void updatePolicyAfterPayment(Payment payment) {
Policy policy = payment.getPolicy();
if (payment.getType() == PaymentType.PREMIUM) {
// 保费支付成功后更新保单状态
if (policy.getStatus() == PolicyStatus.PENDING_PAYMENT) {
policy.setStatus(PolicyStatus.ACTIVE);
policyRepository.save(policy);
// 发送保单激活通知
rabbitTemplate.convertAndSend("policy.activated.queue", policy.getId());
}
}
}
private void initiatePaymentGateway(Payment payment) {
// 这里应该有调用实际支付网关的逻辑
// 简化版示例:发送到消息队列异步处理
rabbitTemplate.convertAndSend("payment.process.queue", payment.getId());
}
@Scheduled(cron = "0 0 9 * * ?") // 每天上午9点执行
public void processDailyReconciliation() {
// 自动下载银行对账文件并处理
log.info("开始执行每日自动对账...");
// 实现逻辑...
}
}
java
public interface CommissionRepository extends JpaRepository<Commission, Long> {
List<Commission> findByPaymentId(Long paymentId);
List<Commission> findByAgentId(Long agentId);
List<Commission> findByStatus(CommissionStatus status);
@Query("SELECT SUM(c.amount) FROM Commission c WHERE c.status = 'PAID' AND c.paidDate BETWEEN :start AND :end")
BigDecimal sumPaidCommissionsBetween(@Param("start") Date start, @Param("end") Date end);
}