欢迎光临
我们一直在努力

保单状态更新:反面案例(直接 update) vs 正确实战(状态机统一流转入口)

目录

一、反面案例:直接 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);
}
}

🚨会发生什么故障?并发时序问题

  • 保单初始状态:01 待核保
  • 线程 1:银行回调扣款成功,执行 updatePolicyEffective()
  • 线程 2:用户同时发起退保,执行 updatePolicySurrender()
    • 时序 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:自研有限状态机(实战最常用)

    核心思想:

  • 所有状态变更只能调用这一个统一入口方法 changeStatus(),项目禁止任何地方直接 update status
  • 配置【当前状态 → 目标状态】合法流转关系
  • 流转前校验:不在合法关系直接抛异常
  • 状态变更记录流水,审计留痕
  • 数据库更新采用乐观锁 + where 条件限制当前状态,防止并发错乱
  • 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");
    }
    }

    ✅这套自研方案解决了反面案例全部问题

  • 不合法流转直接抛出异常,退保终态不能再改成有效;
  • Mapper update 带上 AND status=旧状态,并发情况下,如果另一个线程已经修改状态,update 返回 0,直接报错,杜绝并发覆盖;
  • 全部状态变更集中在一处,新增状态只需要修改 PolicyStateConfig流转配置,不用到处改业务代码;
  • 每次变更记录状态流水表 t_policy_status_log,审计回溯,金融监管要求;
  • 代码约束:团队开发规范,禁止开发手写 update status,code review 重点检查。

  • 方案 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 状态会有什么问题?你项目怎么处理?

  • 如果业务代码 scattered 直接 update 保单 status,并发场景下会出现非法状态跳转,比如已经退保保单被改成有效,造成资金事故;
  • 解决:封装统一状态变更入口,配置合法流转图;所有状态变更必须走这个入口,禁止零散 update;
  • 更新 SQL 带上 where status=旧状态,乐观锁并发控制;
  • 每次状态变更记录状态流水表用于审计;
  • 状态多可以用 Spring‑StateMachine;中小项目自研状态流转配置类足够,轻量可控。
  • 赞(0)
    未经允许不得转载:171主机测评 » 保单状态更新:反面案例(直接 update) vs 正确实战(状态机统一流转入口)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址