一种限额功能的实现

一、背景

业务上需要一个限额功能,以配置的方式,按日为维度去限制某一家通道的授信、放款额度。这个业务刚刚起步规模不大,日进件笔数万级,并发量不高,服务也刚搭建,已满足业务上的基本功能,目前基建不足。产品以紧急需求提出。

二、设计

1、表设计

CREATE TABLE `quota` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `quota_code` varchar(64) NOT NULL COMMENT '限额code',
  `product_code` varchar(32) NOT NULL COMMENT '产品编码',
  `trans_type` varchar(16) NOT NULL COMMENT '交易类型(授信CREDIT/放款LOAN/还款REPAY)',
  `quota` bigint(20) NOT NULL DEFAULT '0' COMMENT '额度(分)',
  `frozen_quota` bigint(20) NOT NULL DEFAULT '0' COMMENT '已冻结额度',
  `used_quota` bigint(20) NOT NULL DEFAULT '0' COMMENT '已使用的额度',
  `start_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '额度有效期起始',
  `end_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '额度有效期结束',
  `status` varchar(16) NOT NULL DEFAULT '0' COMMENT '状态 1 生效、0失效',
  `remark` varchar(4096) DEFAULT NULL COMMENT '备注',
  `creator` varchar(32) NOT NULL DEFAULT 'SYSTEM' COMMENT '创建人',
  `modifier` varchar(32) NOT NULL DEFAULT 'SYSTEM' COMMENT '修改人',
  `gmt_created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `is_deleted` char(1) NOT NULL DEFAULT 'N' COMMENT '是否删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_quota_code` (`quota_code`) USING BTREE,
  KEY `idx_product_code_trans_type` (`product_code`,`trans_type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=171 DEFAULT CHARSET=utf8 COMMENT='限额表';
CREATE TABLE `quota_record` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `quota_code` varchar(64) NOT NULL COMMENT '限额code',
  `business_no` varchar(64) NOT NULL COMMENT '交易编号',
  `product_code` varchar(32) NOT NULL COMMENT '产品编码',
  `trans_type` varchar(16) NOT NULL COMMENT '交易类型(授信CREDIT/放款LOAN/还款REPAY)',
  `amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '交易金额(分)',
  `original_quota` bigint(20) DEFAULT NULL COMMENT '原使用额度(分)',
  `current_quota` bigint(20) DEFAULT NULL COMMENT '现使用额度(分)',
  `trans_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '交易时间',
  `status` varchar(16) NOT NULL DEFAULT '0' COMMENT '状态 SUCCESS 成功、FAIL 失败、INIT 初始化',
  `remark` varchar(4096) DEFAULT NULL COMMENT '备注',
  `creator` varchar(32) NOT NULL DEFAULT 'SYSTEM' COMMENT '创建人',
  `modifier` varchar(32) NOT NULL DEFAULT 'SYSTEM' COMMENT '修改人',
  `gmt_created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `is_deleted` char(1) NOT NULL DEFAULT 'N' COMMENT '是否删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_quota_code_business_no` (`quota_code`,`business_no`) USING BTREE,
  KEY `idx_gmt_created` (`gmt_created`) USING BTREE,
  KEY `idx_business_no_trans_type_status` (`business_no`,`trans_type`,`status`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=139 DEFAULT CHARSET=utf8 COMMENT='限额明细表';

2、系统设计

在这里插入图片描述

三、核心代码

QuotaService.java


/**
 * 限额服务
 * @author majunbo
 */
public interface QuotaService {
    /**
     * 创建额度
     *
     * @param quotaDTO
     * @return
     */
    BaseResp<QuotaDTO> createQuota(QuotaDTO quotaDTO);

    /**
     * 修改额度
     *
     * @param quotaDTO
     * @return
     */
    BaseResp<QuotaDTO> editQuota(QuotaDTO quotaDTO);

    /**
     * 查询额度配置
     *
     * @param quotaDTO
     * @return
     */
    BaseResp<List<QuotaDTO>> query(QuotaDTO quotaDTO);

    /**
     * 额度冻结
     * @param productCode 产品编号
     * @param businessNo 业务流水号
     * @param transType 交易类型(授信CREDIT、放款LOAN)
     * @param amount 交易金额
     * @param applyTime 交易时间
     * @return
     */
    boolean freezeQuota(String productCode, String businessNo, String transType, Long amount, Date applyTime);

    /**
     * 额度更新
     *
     * @param businessNo
     * @param transType
     * @param status
     * @return
     */
    boolean updateQuota(String businessNo, String transType, String status);

    void update(QuotaDO quotaFromDb, QuotaRecordDO quotaRecordFromDb, String status);

    void freeze(QuotaDO quotaFromDb, Long amount, String businessNo, String transType);

}

QuotaServiceImpl.java

/**
 * 限额服务实现类
 * @author majunbo
 */
@Service
@Slf4j
public class QuotaServiceImpl implements QuotaService {

    @Resource
    private Sequence    sequence;

    @Resource
    private QuotaMapper quotaMapper;

    @Resource
    private QuotaRecordMapper quotaRecordMapper;

    @Resource
    private JedisUtil jedisUtil;

    private final String REDIS_LOCK_KEY = "quotaLock:";

    private final Integer REDIS_EXPIRE_TIME = 30;

    private final String STATUS_SUCCESS ="SUCCESS";

    private final String STATUS_INIT ="INIT";

    private final String STATUS_FAIL ="FAIL";

    @Override
    public BaseResp<QuotaDTO> createQuota(QuotaDTO quotaDTO) {
        log.info("QuotaServiceImpl createQuota begin,quotaDTO={}", quotaDTO);
        ValidatorUtil.validate(quotaDTO);
        try {
            QuotaDO quotaDO = initQuotaDO(quotaDTO);
            int result = quotaMapper.insertQuota(quotaDO);
            log.info("QuotaServiceImpl createQuota success,quotaDTO={}", quotaDTO);
            if (result > 0) {
                return BaseResp.success();
            }
            log.error("QuotaServiceImpl createQuota fail,quotaDTO={}", quotaDTO);
            return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "额度配置落库失败");
        } catch (Exception e) {
            log.error("QuotaServiceImpl createQuota fail,quotaDTO={}", quotaDTO, e);
            return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "额度配置落库失败");
        }
    }

    @Override
    public BaseResp<QuotaDTO> editQuota(QuotaDTO quotaDTO) {
        log.info("QuotaServiceImpl editQuota begin,quotaDTO={}", quotaDTO);
        //1.入参校验
        ValidatorUtil.validate(quotaDTO);
        if (ObjectUtils.isEmpty(quotaDTO.getId()) || ObjectUtils.isEmpty(quotaDTO.getStatus())) {
            log.error("QuotaServiceImpl editQuota id、status不能为空,quotaDTO={}", quotaDTO);
            return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "id、status不能为空");
        }
        //2.查本地记录
        QuotaDO quotaDO = initQuotaDO(quotaDTO);
        QuotaDO quotaFromDb = quotaMapper.selectById(quotaDO.getId());
        if (null == quotaFromDb) {
            log.error("QuotaServiceImpl editQuota 查无此限额配置,quotaDTO={}", quotaDTO);
            return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "查无此限额配置");
        }
        //3.校验时间是否存在交叉
        if (!ObjectUtils.isEmpty(quotaDTO.getStatus()) && "1".equals(quotaDTO.getStatus())) {
            checkTimeCross(quotaDO);
        }
        //4.redis分布式自旋锁
        String lockKey = REDIS_LOCK_KEY + quotaFromDb.getQuotaCode();
        try {
            int times = 0;
            while (!jedisUtil.tryLock(lockKey, REDIS_EXPIRE_TIME)) {
                Thread.sleep(50);
                times++;
                if (times > 10) {
                    log.info("QuotaServiceImpl editQuota 无法获取锁 redisKey:[{}] quotaDTO:[{}]", lockKey, quotaDTO);
                    return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "无法获取锁,请稍后重试");
                }
            }
            quotaFromDb = quotaMapper.selectById(quotaDO.getId());
            //5.校验修改后额度 >= 冻结额度 + 已使用额度
            if (quotaDO.getQuota() < quotaFromDb.getUsedQuota() + quotaFromDb.getFrozenQuota()) {
                log.error("QuotaServiceImpl editQuota 额度小于冻结额度+已使用额度,quotaDTO={}", quotaDTO);
                jedisUtil.releaseLock(lockKey);
                return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "额度小于冻结额度+已使用额度");
            }
            //6.修改限额
            quotaDO.setGmtModified(new Date());
            int result = quotaMapper.updateByPrimaryKeySelective(quotaDO);
            //7.释放锁
            jedisUtil.releaseLock(lockKey);
            log.info("QuotaServiceImpl editQuota success,quotaDTO={}", quotaDO);
            if (result > 0) {
                return BaseResp.success();
            }
            return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "额度修改落库失败");
        } catch (Exception e) {
            jedisUtil.releaseLock(lockKey);
            log.error("QuotaServiceImpl editQuota 额度修改失败,quotaDTO={}", quotaDTO, e);
            return BaseResp.fail(RespCodeEnum.FAIL.getCode(), "额度修改失败");
        }
    }

    @Override
    public BaseResp<List<QuotaDTO>> query(QuotaDTO quotaDTO) {
        QuotaDO quotaDO = new QuotaDO();
        BeanUtils.copyProperties(quotaDTO, quotaDO);
        List<QuotaDO> quotaDOS = quotaMapper.selectByDO(quotaDO);
        if(ObjectUtils.isEmpty(quotaDOS)){
            return BaseResp.success(new ArrayList<>());
        }
        List<QuotaDTO> quotaDTOS = quotaDOS.stream().map(this::toDTO).collect(Collectors.toList());
        return BaseResp.success(quotaDTOS);
    }

    @Override
    public boolean freezeQuota(String productCode, String businessNo, String transType, Long amount, Date applyTime) {
        log.info("QuotaServiceImpl freezeQuota begin,productCode={},bussinessNo={},transType={},amount={},applyTime={}",
                productCode, businessNo, transType, amount, applyTime);
        //1.查生效限额配置
        QuotaDO quotaFromDb = queryQuota(productCode, transType, applyTime);
        if (ObjectUtils.isEmpty(quotaFromDb)) {
            log.info("QuotaServiceImpl freezeQuota 没有查到生效的额度配置,默认成功,productCode={},transType={},amount={}", productCode,
                    transType, amount);
            return true;
        }
        String lockKey = REDIS_LOCK_KEY + quotaFromDb.getQuotaCode();
        try {
            //2.加锁
            int times = 0;
            while (!jedisUtil.tryLock(lockKey, REDIS_EXPIRE_TIME)) {
                Thread.sleep(50);
                times++;
                if (times > 10) {
                    //如果拿不到锁直接通过不限额
                    log.info("QuotaServiceImpl freezeQuota 无法获取锁 redisKey:[{}] quotaFromDb:[{}]", lockKey, quotaFromDb);
                    return true;
                }
            }
            //3.校验是否超额
            quotaFromDb = quotaMapper.selectById(quotaFromDb.getId());
            log.info("QuotaServiceImpl freezeQuota quotaFromDb={}", quotaFromDb);
            if (amount <= quotaFromDb.getQuota() - quotaFromDb.getUsedQuota() - quotaFromDb.getFrozenQuota()) {
                ((QuotaService) AopContext.currentProxy()).freeze(quotaFromDb, amount, businessNo, transType);
                jedisUtil.releaseLock(lockKey);
                return true;
            }
            log.info("QuotaServiceImpl freezeQuota 剩余额度不足,amount={},quotaFromDb={}", amount, quotaFromDb);
            jedisUtil.releaseLock(lockKey);
            return false;
        } catch (Exception e) {
            log.error("QuotaServiceImpl freezeQuota 额度冻结失败,productCode={},transType={},amount={}", productCode,
                    transType, amount, e);
            jedisUtil.releaseLock(lockKey);
            throw new BusinessException(GatewayRespCodeEnum.FAIL.getCode(), "额度冻结失败");
        }
    }

    @Override
    public boolean updateQuota(String businessNo, String transType, String status) {
        log.info("QuotaServiceImpl updateQuota begin,businessNo={},transType={},status={}", businessNo, transType,
                status);
        //1.查询限额明细
        QuotaRecordDO quotaRecordFromDb = getQuotaRecord(businessNo, transType);
        if (null == quotaRecordFromDb) {
            log.info("QuotaServiceImpl updateQuota 未查到限额明细,默认成功,businessNo={},transType={}", businessNo, transType);
            return true;
        }
        String lockKey = REDIS_LOCK_KEY + quotaRecordFromDb.getQuotaCode();
        try {
            //2.分布式锁
            int times = 0;
            while (!jedisUtil.tryLock(lockKey, REDIS_EXPIRE_TIME)) {
                Thread.sleep(50);
                times++;
                if (times > 10) {
                    log.info("QuotaServiceImpl updateQuota 无法获取锁 redisKey:[{}] quotaRecordFromDb:[{}]", lockKey,
                            quotaRecordFromDb);
                    return false;
                }
            }
            //查询限额配置
            QuotaDO quotaFromDb = queryQuota(quotaRecordFromDb.getQuotaCode());
            ((QuotaService) AopContext.currentProxy()).update(quotaFromDb,quotaRecordFromDb,status);
            jedisUtil.releaseLock(lockKey);
            return true;
        } catch (Exception e) {
            log.error("QuotaServiceImpl updateQuota 限额更新失败,businessNo={},transType={},status={}", businessNo, transType,
                    status,e);
            jedisUtil.releaseLock(lockKey);
            throw new BusinessException(GatewayRespCodeEnum.FAIL.getCode(), "额度更新失败");
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class, value = "TxMgr")
    public void update(QuotaDO quotaFromDb,QuotaRecordDO quotaRecordFromDb,String status){
        QuotaRecordDO updateQuotaRecord = new QuotaRecordDO();
        updateQuotaRecord.setId(quotaRecordFromDb.getId());
        QuotaDO quotaDO = new QuotaDO();
        quotaDO.setId(quotaFromDb.getId());
        if (STATUS_SUCCESS.equals(status)) {
            //放款成功
            //3.更新限额明细
            updateQuotaRecord.setOriginalQuota(quotaFromDb.getUsedQuota());
            updateQuotaRecord.setCurrentQuota(quotaFromDb.getUsedQuota() + quotaRecordFromDb.getAmount());
            updateQuotaRecord.setStatus(STATUS_SUCCESS);
            updateQuotaRecord.setGmtModified(new Date());
            quotaRecordMapper.updateByPrimaryKeySelective(updateQuotaRecord);
            //4.更新限额
            quotaDO.setFrozenQuota(quotaFromDb.getFrozenQuota() - quotaRecordFromDb.getAmount());
            quotaDO.setUsedQuota(quotaFromDb.getUsedQuota() + quotaRecordFromDb.getAmount());
            quotaDO.setGmtModified(new Date());
            quotaMapper.updateByPrimaryKeySelective(quotaDO);
        } else {
            //放款失败
            //3.更新限额明细
            updateQuotaRecord.setStatus(STATUS_FAIL);
            updateQuotaRecord.setGmtModified(new Date());
            quotaRecordMapper.updateByPrimaryKeySelective(updateQuotaRecord);
            //4.更新限额
            quotaDO.setFrozenQuota(quotaFromDb.getFrozenQuota() - quotaRecordFromDb.getAmount());
            quotaDO.setGmtModified(new Date());
            quotaMapper.updateByPrimaryKeySelective(quotaDO);
        }
        log.info("QuotaServiceImpl updateQuota 限额更新成功,status={},quotaDO={},quotaRecord={}", status, quotaDO,
                updateQuotaRecord);
    }

    @Override
    @Transactional(rollbackFor = Exception.class, value = "TxMgr")
    public void freeze(QuotaDO quotaFromDb, Long amount, String businessNo, String transType) {
        //4.更新限额
        QuotaDO updateQuota = new QuotaDO();
        updateQuota.setId(quotaFromDb.getId());
        updateQuota.setFrozenQuota(quotaFromDb.getFrozenQuota() + amount);
        updateQuota.setGmtModified(new Date());
        quotaMapper.updateByPrimaryKeySelective(updateQuota);
        //5.新增限额记录
        QuotaRecordDO quotaRecordDO = initQuotaRecordDO(quotaFromDb, businessNo, transType, amount);
        quotaRecordMapper.insertSelective(quotaRecordDO);
        log.info("QuotaServiceImpl freezeQuota 冻结成功,updateQuota={}", updateQuota);
    }

    QuotaDO initQuotaDO(QuotaDTO quotaDTO) {
        QuotaDO quotaDO = new QuotaDO();
        BeanUtils.copyProperties(quotaDTO, quotaDO);
        quotaDO.setStartTime(DateUtil.string2Date(quotaDTO.getStartTime(), DateUtil.DATE_FORMAT_YYYYMMDD_HHMMSS));
        quotaDO.setEndTime(DateUtil.string2Date(quotaDTO.getEndTime(), DateUtil.DATE_FORMAT_YYYYMMDD_HHMMSS));
        if (ObjectUtils.isEmpty(quotaDTO.getId())) {
            quotaDO.setQuotaCode(
                    quotaDTO.getProductCode() + DateUtil.date2String(new Date(), DateUtil.DATE_FORMAT_YYYYMMDD) + String
                            .format("%08d", SequenceUtil.getId(sequence)));
        }
        return quotaDO;
    }

    void checkTimeCross(QuotaDO quotaDO) {
        QuotaDO record = new QuotaDO();
        record.setProductCode(quotaDO.getProductCode());
        record.setTransType(quotaDO.getTransType());
        record.setStatus("1");
        List<QuotaDO> records = quotaMapper.selectByDO(record);
        boolean timeCross = false;
        Date start = quotaDO.getStartTime();
        Date end = quotaDO.getEndTime();
        if (!ObjectUtils.isEmpty(records)) {
            //判断时间是否交叉
            //日期不重合场景
            //第1种:日期段1 开始日 < 日期段2 开始日,且 日期段1 结束日 < 日期段2 开始日
            //第2种:日期段1 开始日 > 日期段2 开始日,且 日期段1 开始日 > 日期段2 结束日
            for (QuotaDO qu : records) {
                if (!quotaDO.getId().equals(qu.getId())) {
                    if (start.getTime() < qu.getStartTime().getTime() && end.getTime() <= qu.getStartTime().getTime()) {
                        continue;
                    }
                    if (start.getTime() > qu.getStartTime().getTime() && start.getTime() >= qu.getEndTime().getTime()) {
                        continue;
                    }
                    timeCross = true;
                }
            }
        }
        if (timeCross) {
            log.error("QuotaServiceImpl editQuota 同一产品时间段内只能存在唯一生效配置,quotaDO={}", quotaDO);
            throw new BusinessException(RespCodeEnum.FAIL.getCode(), "同一产品时间段内只能存在唯一生效配置");
        }
    }

    public QuotaDO queryQuota(String productCode, String transType,Date applyTime) {
        QuotaDO quotaDO = new QuotaDO();
        quotaDO.setProductCode(productCode);
        quotaDO.setTransType(transType);
        quotaDO.setStatus("1");
        quotaDO.setGmtCreated(applyTime);
        return quotaMapper.queryQuota(quotaDO);
    }

    public QuotaDO queryQuota(String quotaCode) {
        QuotaDO quotaDO = new QuotaDO();
        quotaDO.setQuotaCode(quotaCode);
        return quotaMapper.queryQuotaByCode(quotaDO);
    }

    QuotaRecordDO initQuotaRecordDO(QuotaDO quotaFromDb, String businessNo, String transType, Long amount) {
        QuotaRecordDO quotaRecordDO = new QuotaRecordDO();
        quotaRecordDO.setQuotaCode(quotaFromDb.getQuotaCode());
        quotaRecordDO.setProductCode(quotaFromDb.getProductCode());
        quotaRecordDO.setBusinessNo(businessNo);
        quotaRecordDO.setTransType(transType);
        quotaRecordDO.setAmount(amount);
        quotaRecordDO.setOriginalQuota(null);
        quotaRecordDO.setCurrentQuota(null);
        quotaRecordDO.setTransTime(new Date());
        quotaRecordDO.setStatus(STATUS_INIT);
        return quotaRecordDO;
    }

    QuotaRecordDO getQuotaRecord(String businessNo, String transType) {
        QuotaRecordDO quotaRecordDO = new QuotaRecordDO();
        quotaRecordDO.setBusinessNo(businessNo);
        quotaRecordDO.setTransType(transType);
        quotaRecordDO.setStatus(STATUS_INIT);
        return quotaRecordMapper.selectRecord(quotaRecordDO);
    }

    private QuotaDTO toDTO(QuotaDO quotaDO) {
        QuotaDTO dto = new QuotaDTO();
        BeanUtils.copyProperties(quotaDO, dto);
        return dto;
    }
}

QuotaDO.java

/**
 * 限额表
 * @author majunbo
 * @TableName quota
 */
@Data
public class QuotaDO implements Serializable {
private static final long serialVersionUID = 1L;
    /**
     * 主键
     */
    private Long id;

    /**
     * 限额code
     */
    private String quotaCode;

    /**
     * 产品编码
     */
    private String productCode;

    /**
     * 交易类型(授信CREDIT/放款LOAN)
     */
    private String transType;

    /**
     * 额度(分)
     */
    private Long quota;

    /**
     * 已冻结额度
     */
    private Long frozenQuota;

    /**
     * 已使用的额度
     */
    private Long usedQuota;

    /**
     * 额度有效期起始
     */
    private Date startTime;

    /**
     * 额度有效期结束
     */
    private Date endTime;

    /**
     * 状态 1 生效、0失效
     */
    private String status;

    /**
     * 备注
     */
    private String remark;

    /**
     * 创建人
     */
    private String creator;

    /**
     * 修改人
     */
    private String modifier;

    /**
     * 创建时间
     */
    private Date gmtCreated;

    /**
     * 修改时间
     */
    private Date gmtModified;

    /**
     * 是否删除
     */
    private String isDeleted;

    
}

QuotaDTO.java

/**
 * @author majunbo
 * @date 2022/9/7 14:14
 */
@Data
public class QuotaDTO implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 主键
     */
    private Long id;

    /**
     * 限额code
     */
    private String quotaCode;

    /**
     * 产品编码
     */
    @NotEmpty(message = "productCode is empty")
    private String productCode;

    /**
     * 交易类型(授信CREDIT/放款LOAN)
     */
    @NotEmpty(message = "transType is empty")
    @EnumCodeValid(values = {"CREDIT","LOAN"}, message = "transType param error")
    private String transType;

    /**
     * 额度(分)
     */
    @NotNull(message = "quota is empty")
    @Range(message = "quota range error")
    private Long quota;

    /**
     * 已冻结额度
     */
    private Long frozenQuota;

    /**
     * 已使用的额度
     */
    private Long usedQuota;

    /**
     * 额度有效期起始
     */
    @NotEmpty(message = "startTime is empty")
    private String startTime;

    /**
     * 额度有效期结束
     */
    @NotEmpty(message = "endTime is empty")
    private String endTime;

    /**
     * 状态 1 生效、0失效
     */
    private String status;

    /**
     * 备注
     */
    private String remark;

    /**
     * 创建人
     */
    private String creator;

    /**
     * 修改人
     */
    private String modifier;

    /**
     * 创建时间
     */
    private Date gmtCreated;

    /**
     * 修改时间
     */
    private Date gmtModified;

    /**
     * 是否删除
     */
    private String isDeleted;
}

QuotaRecordDO.java

/**
 * 限额明细表
 * @author majunbo
 * @TableName quota_record
 */
@Data
public class QuotaRecordDO implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 主键
     */
    private Long id;

    /**
     * 限额code
     */
    private String quotaCode;

    /**
     * 交易编号
     */
    private String businessNo;

    /**
     * 产品编码
     */
    private String productCode;

    /**
     * 交易类型(授信CREDIT/放款LOAN)
     */
    private String transType;

    /**
     * 交易金额(分)
     */
    private Long amount;

    /**
     * 原使用额度(分)
     */
    private Long originalQuota;

    /**
     * 现使用额度(分)
     */
    private Long currentQuota;

    /**
     * 交易时间
     */
    private Date transTime;

    /**
     * 状态 SUCCESS 成功、FAIL 失败、INIT 初始化
     */
    private String status;

    /**
     * 备注
     */
    private String remark;

    /**
     * 创建人
     */
    private String creator;

    /**
     * 修改人
     */
    private String modifier;

    /**
     * 创建时间
     */
    private Date gmtCreated;

    /**
     * 修改时间
     */
    private Date gmtModified;

    /**
     * 是否删除
     */
    private String isDeleted;


四、个人思考和后记

半夜失眠,起来随便写点东西。
限额这块功能在业务上是正常跑着,因为时间的关系从设计到实现难免有些简陋和粗糙,以微服务架构来说,还是拆出来完善一下形成一个单独的限额服务比较好,但考虑到现有的业务规模、成本、需求、时间,单独做一套限额服务难免有过度设计的嫌疑,目前这样设计已经绰绰有余,如果这业务真能做起来再说罢。

关于冻结额度时无法获取锁的处理,我一开始是设计成无法获取锁就返回系统繁忙请重试的code,正好有这个返回码拿来用一下,让上游重新发起请求就行。最后跟产品和leader过到这块时,交流下来他们的想法是拿不到锁就不限额直接放行,估计是有业务上的考虑,那肯定是跟着需求走了,我只是觉得这里有点不妥,只能注释记一下防止之后踩坑罢。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值