puzhibing
2026-09-30 e03f9a706ba46df72c1914870a0a707d6e5e676b
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java
@@ -14,10 +14,12 @@
import com.ruoyi.account.api.dto.ChargingCardSaveDTO;
import com.ruoyi.account.api.dto.ChargingCardStatusDTO;
import com.ruoyi.account.api.model.TChargingCard;
import com.ruoyi.account.api.model.TCompanyAllocateRecord;
import com.ruoyi.account.api.vo.ChargingCardAllocateVO;
import com.ruoyi.account.api.vo.ChargingCardDeductVO;
import com.ruoyi.account.mapper.TChargingCardMapper;
import com.ruoyi.account.service.TChargingCardService;
import com.ruoyi.account.service.TCompanyAllocateRecordService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.StringUtils;
@@ -69,6 +71,14 @@
    private static final int PASSWORD_MAX_LENGTH = 64;
    /** 公司账户模式:2=分配模式(可设置初始分配额度并扣减公司可用余额) */
    private static final int ACCOUNT_MODE_ALLOCATION = 2;
    /** 公司账户模式:1=共享模式(可用余额取公司可用余额) */
    private static final int ACCOUNT_MODE_SHARED = 1;
    /** 账户状态:正常 */
    private static final int ACCOUNT_STATUS_NORMAL = 1;
    /** 账户状态:欠款 */
    private static final int ACCOUNT_STATUS_OVERDUE = 2;
    /** 金额展示精度 */
    private static final int AMOUNT_SCALE = 2;
    /** 初始分配额度上限(对应 decimal(12,2)) */
    private static final BigDecimal MAX_INIT_QUOTA = new BigDecimal("99999999.99");
    /** 额度分配:单次最多分配的卡片数量,防止单次请求过大 */
@@ -91,6 +101,9 @@
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private TCompanyAllocateRecordService companyAllocateRecordService;
    @Override
    public IPage<TChargingCard> selectCardPage(ChargingCardQueryDTO query) {
        ChargingCardQueryDTO safeQuery = query == null ? new ChargingCardQueryDTO() : query;
@@ -98,7 +111,9 @@
        long pageSize = safeQuery.getPageSize() == null || safeQuery.getPageSize() < 1 ? 10 : safeQuery.getPageSize();
        Page<TChargingCard> page = new Page<>(pageNum, pageSize);
        IPage<TChargingCard> result = baseMapper.selectCardPage(page, safeQuery);
        fillRelationNames(result == null ? null : result.getRecords());
        List<TChargingCard> records = result == null ? null : result.getRecords();
        fillFleetNames(records);
        fillAccountInfo(records);
        return result;
    }
@@ -106,7 +121,7 @@
    public boolean saveCard(ChargingCardSaveDTO dto) {
        validateCard(dto, false);
        String cardNo = dto.getCardNo().trim();
        BigDecimal quota = normalizeQuota(dto.getInitQuota());
        BigDecimal quota = normalizeQuota(dto.getBalance());
        // 同卡号并发新增串行化,卡号重复校验与额度扣减在同一临界区内
        RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + "CARD_NO:" + cardNo);
        boolean locked = tryQuotaLock(lock, "该卡号正在处理中,请稍后重试");
@@ -114,7 +129,7 @@
        try {
            TChargingCard card = new TChargingCard();
            card.setCardNo(cardNo);
            card.setBalance(dto.getBalance() == null ? BigDecimal.ZERO : dto.getBalance());
            card.setBalance(quota.signum() > 0 ? quota : BigDecimal.ZERO);
            card.setStatus(dto.getStatus() == null ? ENABLED : dto.getStatus());
            card.setPassword(encodePasswordIfPresent(dto.getPassword()));
            card.setChargingFlag(0);
@@ -128,10 +143,11 @@
                deductBalance(card.getCompanyId(), quota);
                compensations.add(() -> refundQuietly(card.getCompanyId(), quota));
            }
            card.setInitQuota(quota.signum() > 0 ? quota : null);
            if (!save(card)) {
                throw new ServiceException("新增卡片失败");
            }
            recordCardAllocate(card.getCardNo(), card.getCompanyId(), BigDecimal.ZERO, quota,
                    TCompanyAllocateRecord.SOURCE_CREATE, "新增卡片初始分配额度");
            return true;
        } catch (RuntimeException ex) {
            compensate(compensations);
@@ -153,6 +169,8 @@
        RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + dto.getId());
        boolean locked = tryQuotaLock(lock, "该卡片额度正在处理中,请稍后重试");
        List<Runnable> compensations = new ArrayList<>();
        // 分配记录延迟到卡片落库成功后写入:失败时公司余额已回补,不应留下流水
        List<Runnable> recordTasks = new ArrayList<>();
        try {
            TChargingCard latest = getById(dto.getId());
            if (latest == null) {
@@ -166,22 +184,26 @@
            card.setUpdateTime(LocalDateTime.now());
            card.setUpdateBy(SecurityUtils.getUserId());
            applyRelation(card, dto);
            BigDecimal newQuota = normalizeQuota(dto.getInitQuota());
            BigDecimal newQuota = normalizeQuota(dto.getBalance());
            if (newQuota.signum() > 0) {
                requireAllocationCompany(card.getCompanyId());
            }
            Integer oldCompanyId = latest.getCompanyId();
            BigDecimal oldQuota = normalizeQuota(latest.getInitQuota());
            BigDecimal oldQuota = normalizeQuota(latest.getBalance());
            if (Objects.equals(oldCompanyId, card.getCompanyId())) {
                // 同一公司按差额调整:调增扣差额,调减退差额
                BigDecimal diff = newQuota.subtract(oldQuota);
                if (diff.signum() > 0 && card.getCompanyId() != null) {
                    deductBalance(card.getCompanyId(), diff);
                    compensations.add(() -> refundQuietly(card.getCompanyId(), diff));
                    recordTasks.add(() -> recordCardAllocate(card.getCardNo(), card.getCompanyId(), oldQuota, diff,
                            TCompanyAllocateRecord.SOURCE_EDIT, "编辑卡片调整初始分配额度"));
                } else if (diff.signum() < 0 && card.getCompanyId() != null) {
                    BigDecimal refund = diff.negate();
                    refundBalance(card.getCompanyId(), refund);
                    compensations.add(() -> deductQuietly(card.getCompanyId(), refund));
                    recordTasks.add(() -> recordCardAllocate(card.getCardNo(), card.getCompanyId(), oldQuota, diff,
                            TCompanyAllocateRecord.SOURCE_EDIT, "编辑卡片调减初始分配额度"));
                } else if (diff.signum() != 0) {
                    log.warn("卡片{}额度变化但未关联公司,跳过公司余额处理,oldQuota={}, newQuota={}",
                            dto.getId(), oldQuota, newQuota);
@@ -191,17 +213,22 @@
                if (oldCompanyId != null && oldQuota.signum() > 0) {
                    refundBalance(oldCompanyId, oldQuota);
                    compensations.add(() -> deductQuietly(oldCompanyId, oldQuota));
                    recordTasks.add(() -> recordCardAllocate(card.getCardNo(), oldCompanyId, oldQuota,
                            oldQuota.negate(), TCompanyAllocateRecord.SOURCE_EDIT, "编辑卡片调整所属公司(原公司退还)"));
                }
                if (card.getCompanyId() != null && newQuota.signum() > 0) {
                    deductBalance(card.getCompanyId(), newQuota);
                    compensations.add(() -> refundQuietly(card.getCompanyId(), newQuota));
                    recordTasks.add(() -> recordCardAllocate(card.getCardNo(), card.getCompanyId(), BigDecimal.ZERO,
                            newQuota, TCompanyAllocateRecord.SOURCE_EDIT, "编辑卡片调整所属公司"));
                }
            }
            card.setInitQuota(newQuota.signum() > 0 ? newQuota : null);
            card.setBalance(newQuota.signum() > 0 ? newQuota : null);
            String encodedPassword = encodePasswordIfPresent(dto.getPassword());
            if (!updateCardRecord(card, encodedPassword, latest)) {
                throw new ServiceException("修改卡片失败");
            }
            recordTasks.forEach(Runnable::run);
            return true;
        } catch (RuntimeException ex) {
            compensate(compensations);
@@ -226,6 +253,44 @@
            throw new ServiceException("删除卡片失败");
        }
        return true;
    }
    /**
     * 批量解绑车队:仅清空车队归属(fleet_id 置空),公司、余额、状态等不受影响;
     * 未绑定车队的选中记录忽略不计
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int unbindFleet(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            throw new ServiceException("请选择需要解绑车队的卡片");
        }
        List<Long> distinct = ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (distinct.isEmpty()) {
            throw new ServiceException("请选择需要解绑车队的卡片");
        }
        List<TChargingCard> records = listByIds(distinct);
        if (records.size() != distinct.size()) {
            throw new ServiceException("部分卡片不存在或已删除");
        }
        List<Long> boundIds = records.stream()
                .filter(card -> card.getFleetId() != null)
                .map(TChargingCard::getId)
                .collect(Collectors.toList());
        if (boundIds.isEmpty()) {
            return 0;
        }
        // 显式 set null 清空车队归属(MyBatis-Plus 字段策略会忽略实体 null,需走 UpdateWrapper)
        boolean updated = lambdaUpdate()
                .in(TChargingCard::getId, boundIds)
                .set(TChargingCard::getFleetId, null)
                .set(TChargingCard::getUpdateTime, LocalDateTime.now())
                .set(TChargingCard::getUpdateBy, SecurityUtils.getUserId())
                .update();
        if (!updated) {
            throw new ServiceException("卡片车队解绑失败,请重试");
        }
        return boundIds.size();
    }
    @Override
@@ -303,6 +368,7 @@
            try {
                for (TChargingCard card : cards) {
                    BigDecimal amount = amounts.get(card.getId());
                    BigDecimal beforeBalance = card.getBalance() == null ? BigDecimal.ZERO : card.getBalance();
                    int updated = baseMapper.adjustBalance(Collections.singletonList(card.getId()), amount,
                            BALANCE_ADD, MAX_AMOUNT, SecurityUtils.getUserId());
                    if (updated != 1) {
@@ -310,6 +376,8 @@
                    }
                    baseMapper.insertBalanceRecord(card.getId(), BALANCE_ADD, amount, remark,
                            SecurityUtils.getUserId());
                    recordCardAllocate(card.getCardNo(), companyId, beforeBalance, amount,
                            TCompanyAllocateRecord.SOURCE_ALLOCATE, remark);
                }
            } catch (RuntimeException ex) {
                // 卡片改动随事务回滚,公司余额为远程扣减需显式退还
@@ -326,7 +394,13 @@
    @Override
    public List<ChargingCardExcelDTO> selectExportList(ChargingCardQueryDTO query) {
        return baseMapper.selectExportList(query == null ? new ChargingCardQueryDTO() : query);
        List<ChargingCardExcelDTO> items = baseMapper.selectExportList(query == null ? new ChargingCardQueryDTO() : query);
        if (items == null || items.isEmpty()) {
            return items;
        }
        fillExportFleetNames(items);
        fillExportAccountInfo(items);
        return items;
    }
    @Override
@@ -440,6 +514,16 @@
    }
    @Override
    public List<TChargingCard> listByCompany(Integer companyId) {
        if (companyId == null) {
            return java.util.Collections.emptyList();
        }
        return lambdaQuery().eq(TChargingCard::getCompanyId, companyId)
                .orderByDesc(TChargingCard::getId)
                .list();
    }
    @Override
    public ChargingCardDeductVO deductBalanceForCharging(ChargingCardDeductDTO dto) {
        validateDeductRequest(dto);
        try {
@@ -479,6 +563,96 @@
            vo.setDuplicated(true);
            return vo;
        }
    }
    @Override
    public BigDecimal lockBalance(Long cardId, BigDecimal amount) {
        if (cardId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("锁定金额参数错误");
        }
        // 实扣冻结:余额立即扣减并计入锁定金额,余额不足时条件更新命中 0 行
        boolean updated = lambdaUpdate()
                .eq(TChargingCard::getId, cardId)
                .ge(TChargingCard::getBalance, amount)
                .setSql("balance = IFNULL(balance, 0) - " + amount.toPlainString()
                        + ", locked_amount = IFNULL(locked_amount, 0) + " + amount.toPlainString())
                .update();
        if (!updated) {
            throw new ServiceException("卡片余额不足");
        }
        TChargingCard latest = getById(cardId);
        return latest == null || latest.getBalance() == null ? BigDecimal.ZERO : latest.getBalance();
    }
    @Override
    public ChargingCardDeductVO settleBalanceForCharging(ChargingCardDeductDTO dto) {
        validateDeductRequest(dto);
        BigDecimal locked = dto.getLockedAmount() == null ? BigDecimal.ZERO : dto.getLockedAmount();
        if (locked.compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("锁定额度参数错误");
        }
        try {
            return transactionTemplate.execute(status -> {
                TChargingCard card = getById(dto.getCardId());
                if (card == null) {
                    throw new ServiceException("卡片不存在或已删除");
                }
                BigDecimal before = card.getBalance() == null ? BigDecimal.ZERO : card.getBalance();
                // 第一步先占扣款幂等键:与旧扣款接口共用流水唯一索引 uk_order_code,
                // 同一订单并发结算时只允许一个事务插入成功
                baseMapper.insertOrderDeductRecord(dto.getCardId(), dto.getAmount(),
                        dto.getOrderCode(), dto.getOperatorId() == null ? SYSTEM_OPERATOR : dto.getOperatorId());
                // 第二步原子结算:先释放锁定额,再按实际消费扣减,允许扣成负余额(V0.2 定稿)
                boolean updated = lambdaUpdate()
                        .eq(TChargingCard::getId, dto.getCardId())
                        .setSql("balance = IFNULL(balance, 0) + " + locked.toPlainString()
                                + " - " + dto.getAmount().toPlainString()
                                + ", locked_amount = GREATEST(IFNULL(locked_amount, 0) - "
                                + locked.toPlainString() + ", 0)")
                        .update();
                if (!updated) {
                    throw new ServiceException("卡片不存在或已删除,结算失败");
                }
                TChargingCard afterCard = getById(dto.getCardId());
                BigDecimal after = afterCard == null || afterCard.getBalance() == null
                        ? BigDecimal.ZERO : afterCard.getBalance();
                ChargingCardDeductVO vo = new ChargingCardDeductVO();
                vo.setCardId(dto.getCardId());
                vo.setOrderCode(dto.getOrderCode());
                vo.setAmount(dto.getAmount());
                vo.setBeforeBalance(before);
                vo.setAfterBalance(after);
                vo.setDuplicated(false);
                return vo;
            });
        } catch (DuplicateKeyException e) {
            // 唯一键冲突:该订单此前已结算成功(含并发下慢事务先提交),按幂等重复返回,不重复结算
            log.warn("刷卡订单结算幂等命中,不重复结算。订单号={}, 卡片ID={}", dto.getOrderCode(), dto.getCardId());
            ChargingCardDeductVO vo = new ChargingCardDeductVO();
            vo.setCardId(dto.getCardId());
            vo.setOrderCode(dto.getOrderCode());
            vo.setAmount(dto.getAmount());
            vo.setDuplicated(true);
            return vo;
        }
    }
    @Override
    public BigDecimal unlockBalance(Long cardId, BigDecimal amount) {
        if (cardId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("释放金额参数错误");
        }
        // 释放锁定额:余额回补,锁定额度扣减,允许余额超过锁定额(异常数据兜底)
        boolean updated = lambdaUpdate()
                .eq(TChargingCard::getId, cardId)
                .setSql("balance = IFNULL(balance, 0) + " + amount.toPlainString()
                        + ", locked_amount = GREATEST(IFNULL(locked_amount, 0) - " + amount.toPlainString() + ", 0)")
                .update();
        if (!updated) {
            throw new ServiceException("卡片不存在或已删除");
        }
        TChargingCard latest = getById(cardId);
        return latest == null || latest.getBalance() == null ? BigDecimal.ZERO : latest.getBalance();
    }
    /**
@@ -611,6 +785,18 @@
        return amounts;
    }
    /** 写入卡片分配流水(分配为正数、回收为负数;公司为空或金额为0时自动忽略) */
    private void recordCardAllocate(String cardNo, Integer companyId, BigDecimal beforeBalance, BigDecimal amount,
                                    int changeSource, String remark) {
        try {
            companyAllocateRecordService.recordAllocate(TCompanyAllocateRecord.USER_TYPE_CARD, cardNo, null, companyId,
                    changeSource, beforeBalance, amount, remark);
        } catch (RuntimeException ex) {
            // 流水为审计数据,写入失败不阻断卡片额度变更主流程,仅记录日志便于排查
            log.error("卡片{}分配流水写入失败:{}", cardNo, ex.getMessage(), ex);
        }
    }
    /** 组装额度分配流水备注(含来源公司,超长截断以适配 varchar(255)) */
    private String buildAllocateRemark(TGroupCompany company, String userRemark) {
        String companyName = company == null || company.getName() == null ? "" : company.getName();
@@ -700,16 +886,11 @@
        }
    }
    /** 列表回填公司/车队名称(远程失败不影响列表查询) */
    private void fillRelationNames(List<TChargingCard> records) {
    /** 列表回填车队名称(远程失败不影响列表查询) */
    private void fillFleetNames(List<TChargingCard> records) {
        if (records == null || records.isEmpty()) {
            return;
        }
        fillFleetNames(records);
        fillCompanyNames(records);
    }
    private void fillFleetNames(List<TChargingCard> records) {
        List<Integer> fleetIds = records.stream()
                .map(TChargingCard::getFleetId)
                .filter(Objects::nonNull)
@@ -735,27 +916,123 @@
        }
    }
    private void fillCompanyNames(List<TChargingCard> records) {
        Map<Integer, String> nameMap = new HashMap<>();
        for (TChargingCard record : records) {
            Integer companyId = record.getCompanyId();
            if (companyId == null) {
                continue;
            }
            if (nameMap.containsKey(companyId)) {
                record.setCompanyName(nameMap.get(companyId));
                continue;
            }
            String companyName = null;
            try {
                TGroupCompany company = loadCompany(companyId);
                companyName = company == null ? null : company.getName();
            } catch (Exception ex) {
                log.warn("回填公司名称失败,companyId={}", companyId, ex);
            }
            nameMap.put(companyId, companyName);
            record.setCompanyName(companyName);
    /** 列表回填所属公司名称与账户信息:账户模式、可用余额、超额、账户状态(远程失败仅留空,不影响列表查询) */
    private void fillAccountInfo(List<TChargingCard> records) {
        if (records == null || records.isEmpty()) {
            return;
        }
        Map<Integer, TGroupCompany> companyMap = loadCompanyMap(records.stream()
                .map(TChargingCard::getCompanyId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
        for (TChargingCard record : records) {
            TGroupCompany company = companyMap.get(record.getCompanyId());
            Integer accountMode = company == null ? null : company.getAccountMode();
            BigDecimal balance = resolveAvailableBalance(accountMode, record.getBalance(),
                    company == null ? null : company.getAvailableBalance());
            record.setCompanyName(company == null ? null : company.getName());
            record.setAccountMode(accountMode);
            record.setAvailableBalance(balance);
            record.setOverrun(resolveOverrun(balance));
            record.setAccountStatus(resolveAccountStatus(balance));
        }
    }
    /** 导出回填车队名称(远程失败不影响导出) */
    private void fillExportFleetNames(List<ChargingCardExcelDTO> items) {
        List<Integer> fleetIds = items.stream()
                .map(ChargingCardExcelDTO::getFleetId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (fleetIds.isEmpty()) {
            return;
        }
        try {
            R<List<TFleet>> response = groupCompanyClient.listFleetsByIds(fleetIds);
            if (response == null || !R.isSuccess(response) || response.getData() == null) {
                return;
            }
            Map<Integer, String> nameMap = new HashMap<>();
            for (TFleet fleet : response.getData()) {
                if (fleet != null && fleet.getId() != null) {
                    nameMap.put(fleet.getId(), fleet.getFleetName());
                }
            }
            items.forEach(item -> item.setFleetName(nameMap.get(item.getFleetId())));
        } catch (Exception ex) {
            log.warn("导出回填车队名称失败,fleetIds={}", fleetIds, ex);
        }
    }
    /** 导出回填所属公司名称与账户信息(与列表同口径) */
    private void fillExportAccountInfo(List<ChargingCardExcelDTO> items) {
        Map<Integer, TGroupCompany> companyMap = loadCompanyMap(items.stream()
                .map(ChargingCardExcelDTO::getCompanyId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
        for (ChargingCardExcelDTO item : items) {
            TGroupCompany company = companyMap.get(item.getCompanyId());
            Integer accountMode = company == null ? null : company.getAccountMode();
            BigDecimal balance = resolveAvailableBalance(accountMode, item.getBalance(),
                    company == null ? null : company.getAvailableBalance());
            item.setCompanyName(company == null ? null : company.getName());
            item.setAccountMode(accountMode);
            item.setAvailableBalance(balance);
            item.setOverrun(resolveOverrun(balance));
            item.setAccountStatus(resolveAccountStatus(balance));
        }
    }
    /** 按ID批量加载公司(远程失败或服务降级时返回空Map,由调用方按未知处理) */
    private Map<Integer, TGroupCompany> loadCompanyMap(List<Integer> companyIds) {
        List<Integer> distinctIds = companyIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (distinctIds.isEmpty()) {
            return new HashMap<>();
        }
        try {
            R<List<TGroupCompany>> response = groupCompanyClient.listCompaniesByIds(distinctIds);
            if (response == null || !R.isSuccess(response) || response.getData() == null) {
                return new HashMap<>();
            }
            Map<Integer, TGroupCompany> companyMap = new HashMap<>();
            for (TGroupCompany company : response.getData()) {
                if (company != null && company.getId() != null) {
                    companyMap.put(company.getId(), company);
                }
            }
            return companyMap;
        } catch (Exception ex) {
            log.warn("回填公司账户信息失败,companyIds={}", distinctIds, ex);
            return new HashMap<>();
        }
    }
    /** 计算可用余额:分配模式=卡片自身余额,共享模式=公司可用余额,账户模式未知时留空 */
    private BigDecimal resolveAvailableBalance(Integer accountMode, BigDecimal cardBalance, BigDecimal companyBalance) {
        if (Integer.valueOf(ACCOUNT_MODE_ALLOCATION).equals(accountMode)) {
            return cardBalance;
        }
        if (Integer.valueOf(ACCOUNT_MODE_SHARED).equals(accountMode)) {
            return companyBalance;
        }
        return null;
    }
    /** 超额:可用余额为负时取其绝对值,其余情况为空 */
    private BigDecimal resolveOverrun(BigDecimal balance) {
        if (balance == null || balance.signum() >= 0) {
            return null;
        }
        return balance.abs().setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
    }
    /** 账户状态:可用余额为负=欠款,非负=正常,可用余额未知时为空 */
    private Integer resolveAccountStatus(BigDecimal balance) {
        if (balance == null) {
            return null;
        }
        return balance.signum() < 0 ? ACCOUNT_STATUS_OVERDUE : ACCOUNT_STATUS_NORMAL;
    }
    /**
@@ -773,17 +1050,22 @@
        if (!updateById(update)) {
            return false;
        }
        BigDecimal newBalance = card.getBalance();
        boolean balanceChanged = newBalance != null && newBalance.signum() > 0
                && !quotaEquals(existing.getBalance(), newBalance);
        boolean relationChanged = !Objects.equals(existing.getCompanyId(), card.getCompanyId())
                || !Objects.equals(existing.getFleetId(), card.getFleetId())
                || !quotaEquals(existing.getInitQuota(), card.getInitQuota());
                || balanceChanged;
        if (!relationChanged) {
            return true;
        }
        UpdateWrapper<TChargingCard> relationUpdate = new UpdateWrapper<>();
        relationUpdate.eq("id", card.getId())
                .set("company_id", card.getCompanyId())
                .set("fleet_id", card.getFleetId())
                .set("init_quota", card.getInitQuota());
                .set("fleet_id", card.getFleetId());
        if (balanceChanged) {
            relationUpdate.set("balance", newBalance);
        }
        return update(null, relationUpdate);
    }