puzhibing
4 天以前 3650ce7f5d42d1bef58abea9693f20d1f209d3b0
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java
@@ -10,10 +10,13 @@
import com.ruoyi.account.api.model.TChargingCard;
import com.ruoyi.account.api.model.TCompanyAllocateRecord;
import com.ruoyi.account.api.model.TVinManagement;
import com.ruoyi.account.api.vo.CompanyAllocateSummaryVO;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.order.api.feignClient.ChargingOrderClient;
import com.ruoyi.other.api.domain.TFleet;
import com.ruoyi.other.api.domain.TGroupCompany;
import com.ruoyi.other.api.model.TCompanyRechargeRecord;
@@ -120,6 +123,10 @@
    /** 企业额度分配记录远程客户端(结算时写消费流水、幂等判定使用) */
    @Resource
    private CompanyAllocateRecordClient companyAllocateRecordClient;
    /** 充电订单远程客户端(账户模式切换前进行中订单校验使用) */
    @Resource
    private ChargingOrderClient chargingOrderClient;
    @Override
    public TGroupCompany getDetail(Integer id) {
@@ -548,12 +555,163 @@
        if (company == null) {
            return AjaxResult.error("公司不存在");
        }
        // 目标模式与当前一致:幂等直接返回
        if (Objects.equals(company.getAccountMode(), accountMode)) {
            return AjaxResult.success();
        }
        // 统一校验:无论切换方向,存在进行中充电订单均拒绝切换
        AjaxResult chargingCheck = ensureNoActiveChargingOrder(id);
        if (chargingCheck != null) {
            return chargingCheck;
        }
        // 共享 → 分配:仅切换模式,不涉及资金回退
        if (accountMode == ACCOUNT_MODE_ALLOCATION) {
        TGroupCompany update = new TGroupCompany();
        update.setId(id);
        update.setAccountMode(accountMode);
        updateById(update);
        return AjaxResult.success();
    }
        // 分配 → 共享:校验在途业务后回收全部分配余额
        return switchAllocationToShared(id, company);
    }
    /**
     * 校验公司无进行中充电订单(三表合计,状态 2=启动中/3=充电中/4=停止中)。
     * 通过返回 null;不通过返回可直接回给前端的错误结果。
     * 账户模式双向切换均须先通过本校验。
     */
    private AjaxResult ensureNoActiveChargingOrder(Integer id) {
        R<Integer> activeCountResult;
        try {
            activeCountResult = chargingOrderClient.countActiveByCompany(id);
        } catch (RuntimeException ex) {
            log.error("[模式切换] 统计进行中充电订单异常 companyId={}", id, ex);
            return AjaxResult.error("校验充电状态失败,请稍后重试");
        }
        if (activeCountResult == null || R.SUCCESS != activeCountResult.getCode()) {
            return AjaxResult.error("校验充电状态失败,请稍后重试");
        }
        Integer activeCount = activeCountResult.getData();
        if (activeCount != null && activeCount > 0) {
            return AjaxResult.error("该公司存在正在充电的订单,暂不允许切换账户模式");
        }
        return null;
    }
    /**
     * 分配模式切换为共享模式:
     * <ol>
     *   <li>校验公司与三类主体均无未释放充电锁定额</li>
     *   <li>公司余额预加回回收总额 + 切换为共享模式(本地事务)</li>
     *   <li>远程回收三类主体全部余额并写双向流水(account 侧锁内事务,幂等)</li>
     *   <li>按实际回收额与预加回额差额校准公司余额(并发分配竞态兜底)</li>
     * </ol>
     * 进行中充电订单校验已在 updateAccountMode 公共入口完成。
     */
    private AjaxResult switchAllocationToShared(Integer id, TGroupCompany company) {
        // 1. 公司锁定额校验(在途充电锁定未释放)
        BigDecimal companyLocked = company.getLockedAmount();
        if (companyLocked != null && companyLocked.signum() > 0) {
            return AjaxResult.error("公司存在未释放的充电锁定金额,暂不允许切换为共享模式");
        }
        // 2. 三类主体余额与锁定额统计
        R<CompanyAllocateSummaryVO> summaryResult;
        try {
            summaryResult = companyAllocateRecordClient.sumAllocatedBalance(id);
        } catch (RuntimeException ex) {
            log.error("[模式切换] 统计分配余额异常 companyId={}", id, ex);
            return AjaxResult.error("查询分配余额失败,请稍后重试");
        }
        if (summaryResult == null || R.SUCCESS != summaryResult.getCode() || summaryResult.getData() == null) {
            return AjaxResult.error("查询分配余额失败,请稍后重试");
        }
        CompanyAllocateSummaryVO summary = summaryResult.getData();
        if (summary.getTotalLocked() != null && summary.getTotalLocked().signum() > 0) {
            return AjaxResult.error("存在充电锁定金额未释放,暂不允许切换为共享模式");
        }
        BigDecimal total = summary.getTotalBalance() == null ? BigDecimal.ZERO : summary.getTotalBalance();
        BigDecimal companyBalanceBefore = company.getAvailableBalance() == null
                ? BigDecimal.ZERO : company.getAvailableBalance();
        // 3. 公司余额加回 + 模式切换(本地事务)
        if (!updateCompanyToShared(id, total)) {
            throw new ServiceException("公司余额更新失败");
        }
        // 4. 远程回收三类主体余额(account 侧三把公司级锁串行、事务、幂等),失败回滚公司余额与模式
        BigDecimal actual;
        try {
            actual = recycleAllocatedBalanceRemote(id, company, companyBalanceBefore);
        } catch (RuntimeException ex) {
            log.error("[模式切换] 远程回收分配余额异常 companyId={}", id, ex);
            rollbackModeSwitch(id, total);
            throw new ServiceException("回收分配余额失败,请稍后重试");
        }
        // 5. 差额调整:回收与统计间存在并发分配时,按实际回收额校准公司余额
        BigDecimal diff = actual.subtract(total);
        if (diff.signum() != 0) {
            boolean adjusted = lambdaUpdate()
                    .eq(TGroupCompany::getId, id)
                    .setSql("available_balance = IFNULL(available_balance, 0) + (" + diff.toPlainString() + ")")
                    .update();
            if (!adjusted) {
                log.error("[模式切换] 公司余额差额调整失败 companyId={} diff={}", id, diff.toPlainString());
            }
        }
        log.info("[模式切换] 分配→共享成功 companyId={} 回收总额={}", id, actual.toPlainString());
        return AjaxResult.success("账户模式已切换为共享模式,回收分配余额 " + actual.toPlainString() + " 元");
    }
    /** 更新公司余额(加回回收额)并切换为共享模式,返回是否更新成功 */
    private boolean updateCompanyToShared(Integer id, BigDecimal total) {
        if (total.signum() > 0) {
            return lambdaUpdate()
                    .eq(TGroupCompany::getId, id)
                    .setSql("available_balance = IFNULL(available_balance, 0) + " + total.toPlainString())
                    .set(TGroupCompany::getAccountMode, ACCOUNT_MODE_SHARED)
                    .update();
        }
        TGroupCompany update = new TGroupCompany();
        update.setId(id);
        update.setAccountMode(ACCOUNT_MODE_SHARED);
        return updateById(update);
    }
    /** 远程回收三类主体分配余额,失败(远程失败码/返回体异常)抛出 ServiceException 由调用方补偿 */
    private BigDecimal recycleAllocatedBalanceRemote(Integer id, TGroupCompany company,
                                                     BigDecimal companyBalanceBefore) {
        String operatorName = null;
        try {
            operatorName = SecurityUtils.getLoginUser().getUsername();
        } catch (Exception ignored) {
            // 无安全上下文时(内部调用)操作人留空
        }
        R<BigDecimal> recycleResult = companyAllocateRecordClient.recycleAllocatedBalance(id, operatorName,
                company.getName(), companyBalanceBefore);
        if (recycleResult == null || R.SUCCESS != recycleResult.getCode() || recycleResult.getData() == null) {
            String msg = recycleResult == null ? "远程调用失败" : recycleResult.getMsg();
            log.error("[模式切换] 回收分配余额失败 companyId={} msg={}", id, msg);
            throw new ServiceException("回收分配余额失败:" + msg);
        }
        return recycleResult.getData();
    }
    /**
     * 回收失败补偿:公司余额减回预加回额、模式恢复为分配模式。
     * 仅当模式仍为共享时执行恢复,避免覆盖后续并发操作;恢复失败仅记录日志(下轮对账/重试兜底)。
     */
    private void rollbackModeSwitch(Integer id, BigDecimal total) {
        try {
            boolean rollbacked = lambdaUpdate()
                    .eq(TGroupCompany::getId, id)
                    .eq(TGroupCompany::getAccountMode, ACCOUNT_MODE_SHARED)
                    .setSql("available_balance = IFNULL(available_balance, 0) - " + total.toPlainString())
                    .set(TGroupCompany::getAccountMode, ACCOUNT_MODE_ALLOCATION)
                    .update();
            log.warn("[模式切换] 回收失败已回滚 companyId={} rollbacked={}", id, rollbacked);
        } catch (RuntimeException ex) {
            log.error("[模式切换] 回收失败回滚异常 companyId={}", id, ex);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)