目录
一、反面案例:直接 update 数据库(线上真实踩坑写法,禁止使用)
Service 错误代码
🚨会发生什么故障?并发时序问题
二、正确实战方案:统一状态流转入口(两种实现)
方案 1:自研有限状态机(实战最常用)
1)状态流转配置
2)统一状态流转 Service 入口
3)Mapper 自定义 SQL(关键 update where 条件)
4)业务层如何调用(投保扣款成功、退保业务)
✅这套自研方案解决了反面案例全部问题
方案 2:Spring‑StateMachine 框架简要示例(大型保险项目)
面试口述要点总结
保单状态枚举
java
/**
* 保单状态枚举
*/
public enum PolicyStatusEnum {
/** 待核保 */
UNDERWRITING("01","待核保"),
/** 有效承保 */
EFFECTIVE("02","有效承保"),
/** 犹豫期 */
HESITATE("03","犹豫期"),
/** 已退保 */
SURRENDER("04","已退保"),
/** 理赔终止 */
CLAIM_END("05","理赔终止");
}
数据库 t_policy 核心字段:policy_id, status, customer_id,…
一、反面案例:直接 update 数据库(线上真实踩坑写法,禁止使用)
问题:到处写 update,没有校验前置条件,可以任意跳转状态,产生非法状态数据。 业务背景:两个并发场景 1)场景 A:投保扣款成功 → 把保单从【待核保】改成【有效承保】 2)场景 B:客户发起退保申请 → 把保单从【待核保】改成【已退保】
Service 错误代码
java
/**
* ❌ 反面案例:直接update保单状态,多处散落更新,无状态校验
*/
@Service
public class BadPolicyService {
@Autowired
private PolicyMapper policyMapper;
/**
* 扣款成功,更新为承保有效
*/
public void updatePolicyEffective(String policyId){
// 直接set状态,没有校验当前是什么状态!
Policy policy = new Policy();
policy.setPolicyId(policyId);
policy.setStatus(PolicyStatusEnum.EFFECTIVE.getCode());
policyMapper.updateById(policy);
}
/**
* 客户申请退保,更新为已退保
*/
public void updatePolicySurrender(String policyId){
// 直接set状态,不校验当前状态
Policy policy = new Policy();
policy.setPolicyId(policyId);
policy.setStatus(PolicyStatusEnum.SURRENDER.getCode());
policyMapper.updateById(policy);
}
}
🚨会发生什么故障?并发时序问题
- 时序 1:线程 1 先执行 → status=02 有效;线程 2 后执行 → 有效保单被直接改成已退保(业务不允许!有效保单退保必须走完整退保计算流程,不能直接改状态)
- 时序 2:线程 2 先执行 → status=04 已退保;线程 1 后执行 → 已经退保的保单被强行改成有效承保!重大线上 bug,资金灾难
根源:直接 update,不校验当前状态,不校验流转是否合法,数据库只执行覆盖,没有约束。 就算你加上简单 if 判断,多个业务方法到处写 if,后期新增状态、新增业务场景,很容易漏写判断,代码腐烂。
java
// 很多人以为加个if就好了,这依然是坏味道
// if(policy.getStatus().equals("01")){update}
// 问题:每个业务方法都要手写一套状态判断,散落在各个service,新增状态就要到处改代码,极易漏。
二、正确实战方案:统一状态流转入口(两种实现)
金融保险项目两种落地: 方案 1:自研简易状态机(项目大多数中小银保项目用,轻量,不引入 Spring‑StateMachine 重依赖) 方案 2:Spring‑StateMachine 框架实现(大型复杂保单系统)
方案 1:自研有限状态机(实战最常用)
核心思想:
1)状态流转配置
java
/**
* 状态流转配置:源状态允许流转到哪些目标状态
*/
public class PolicyStateConfig {
// key:当前状态;value:允许流转的目标状态集合
public static final Map<String, Set<String>> STATE_TRANSFER_MAP = new HashMap<>();
static {
// 待核保 可以转为:有效承保、已退保
STATE_TRANSFER_MAP.put(PolicyStatusEnum.UNDERWRITING.getCode(),
Set.of(PolicyStatusEnum.EFFECTIVE.getCode(), PolicyStatusEnum.SURRENDER.getCode()));
// 有效承保 可以转为:犹豫期、已退保、理赔终止
STATE_TRANSFER_MAP.put(PolicyStatusEnum.EFFECTIVE.getCode(),
Set.of(PolicyStatusEnum.HESITATE.getCode(), PolicyStatusEnum.SURRENDER.getCode(), PolicyStatusEnum.CLAIM_END.getCode()));
// 犹豫期 可以转为:有效承保、已退保
STATE_TRANSFER_MAP.put(PolicyStatusEnum.HESITATE.getCode(),
Set.of(PolicyStatusEnum.EFFECTIVE.getCode(), PolicyStatusEnum.SURRENDER.getCode()));
// 已退保:终态,不允许再流转出去
STATE_TRANSFER_MAP.put(PolicyStatusEnum.SURRENDER.getCode(), Collections.emptySet());
// 理赔终止:终态
STATE_TRANSFER_MAP.put(PolicyStatusEnum.CLAIM_END.getCode(), Collections.emptySet());
}
/**
* 判断是否允许流转
* @param fromStatus 当前状态
* @param toStatus 目标状态
* @return true允许 false不允许
*/
public static boolean isAllowTransfer(String fromStatus, String toStatus){
Set<String> allowSet = STATE_TRANSFER_MAP.get(fromStatus);
if(allowSet == null || allowSet.isEmpty()){
return false;
}
return allowSet.contains(toStatus);
}
}
2)统一状态流转 Service 入口
整个项目所有业务,要修改保单状态,必须调用 changeStatus,禁止自己写 update
java
@Service
public class PolicyStateService {
@Autowired
private PolicyMapper policyMapper;
@Autowired
private PolicyStatusLogMapper statusLogMapper;
/**
* ✅唯一统一状态变更入口
* @param policyId 保单id
* @param targetStatus 目标状态
* @param remark 变更备注
* @param operator 操作人
*/
@Transactional(rollbackFor = Exception.class)
public void changeStatus(String policyId, String targetStatus, String remark, String operator){
//1. 查询保单
Policy policy = policyMapper.selectById(policyId);
if(policy == null){
throw new RuntimeException("保单不存在");
}
String currentStatus = policy.getStatus();
// 状态没变化,直接返回
if(targetStatus.equals(currentStatus)){
return;
}
//2. 校验状态流转合法性,不合法直接抛异常,阻断业务
if(!PolicyStateConfig.isAllowTransfer(currentStatus, targetStatus)){
throw new RuntimeException(
String.format("非法状态流转,当前状态:%s,目标状态:%s", currentStatus, targetStatus));
}
//3. 数据库更新:where条件带上当前状态!乐观锁防并发,只有数据库是currentStatus才更新成功
// 关键!多线程并发情况下,如果数据库状态已经被别的线程修改,update返回0行,代表更新失败
int rows = policyMapper.updateStatusByCondition(policyId, currentStatus, targetStatus);
if(rows == 0){
throw new RuntimeException("保单状态已发生变更,本次流转失败,请重试");
}
//4. 插入状态变更流水记录,审计留痕,金融必做
PolicyStatusLog log = new PolicyStatusLog();
log.setPolicyId(policyId);
log.setFromStatus(currentStatus);
log.setToStatus(targetStatus);
log.setRemark(remark);
log.setOperator(operator);
log.setOperateTime(LocalDateTime.now());
statusLogMapper.insert(log);
}
}
3)Mapper 自定义 SQL(关键 update where 条件)
xml
<!– 根据policyId + 当前旧状态 更新,防止并发覆盖 –>
<update id="updateStatusByCondition">
UPDATE t_policy
SET status = #{targetStatus}
WHERE policy_id = #{policyId}
AND status = #{oldStatus}
</update>
4)业务层如何调用(投保扣款成功、退保业务)
java
@Service
public class PolicyBusinessService {
@Autowired
private PolicyStateService policyStateService;
/**
* 保费扣款成功,保单转为有效承保
*/
public void afterPaySuccess(String policyId){
// 业务逻辑:生成账务、记录保费流水….
// ✅只调用统一入口,不自己写update
policyStateService.changeStatus(policyId,
PolicyStatusEnum.EFFECTIVE.getCode(),
"保费扣款成功,保单承保生效",
"SYSTEM_PAY");
}
/**
* 用户申请退保
*/
public void applySurrender(String policyId){
// 业务逻辑:计算现金价值、生成退保单据、校验退保规则…
// ✅统一入口修改状态
policyStateService.changeStatus(policyId,
PolicyStatusEnum.SURRENDER.getCode(),
"客户发起退保",
"USER_XXX");
}
}
✅这套自研方案解决了反面案例全部问题
方案 2:Spring‑StateMachine 框架简要示例(大型保险项目)
如果状态非常多(20 + 状态)、事件驱动,就用框架。 核心概念:State(状态)、Event(事件)、Transition(流转) 事件定义:投保成功事件、退保事件、复效事件、理赔事件。
java
// 状态机配置
@Configuration
@EnableStateMachine
public class PolicyStateMachineConfig extends StateMachineConfigurerAdapter<String,String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states.withStates()
.initial(PolicyStatusEnum.UNDERWRITING.getCode())
.state(PolicyStatusEnum.EFFECTIVE.getCode())
.state(PolicyStatusEnum.SURRENDER.getCode())
.end(PolicyStatusEnum.SURRENDER.getCode()) //终态
.end(PolicyStatusEnum.CLAIM_END.getCode());
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
// 待核保 –【扣款成功事件PAY_SUCCESS】–> 有效承保
.withExternal().source("01").target("02").event("PAY_SUCCESS")
// 待核保 –【退保事件SURRENDER_EVENT】–>已退保
.and().withExternal().source("01").target("04").event("SURRENDER_EVENT")
// 有效承保 –退保事件 –>已退保
.and().withExternal().source("02").target("04").event("SURRENDER_EVENT");
}
}
业务使用:发送事件驱动流转,框架内部自动校验流转是否合法
java
// 发送扣款成功事件,驱动状态流转
stateMachine.sendEvent("PAY_SUCCESS");
现实项目:Spring‑StateMachine 一般不会直接存状态机内存状态,业务状态依然落 MySQL;状态机只做流转规则校验,状态持久化自己实现。缺点:框架学习成本,调试麻烦,中小银保项目优先自研状态机。
面试口述要点总结
面试官问:保单直接 update 状态会有什么问题?你项目怎么处理?
