From 3650ce7f5d42d1bef58abea9693f20d1f209d3b0 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期六, 10 十月 2026 14:02:27 +0800
Subject: [PATCH] 修改bug
---
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java | 679 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 679 insertions(+), 0 deletions(-)
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java
index f950321..844d3fb 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TGroupCompanyServiceImpl.java
@@ -2,22 +2,49 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.account.api.feignClient.AppUserWhitelistClient;
+import com.ruoyi.account.api.feignClient.ChargingCardClient;
+import com.ruoyi.account.api.feignClient.CompanyAllocateRecordClient;
+import com.ruoyi.account.api.feignClient.VinManagementClient;
+import com.ruoyi.account.api.model.TAppUserWhitelist;
+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;
+import com.ruoyi.other.api.vo.CompanyChargingUserVO;
import com.ruoyi.other.mapper.TCompanyRechargeRecordMapper;
import com.ruoyi.other.mapper.TFleetMapper;
import com.ruoyi.other.mapper.TGroupCompanyMapper;
import com.ruoyi.other.service.TGroupCompanyService;
+import com.ruoyi.system.api.domain.SysUser;
+import com.ruoyi.system.api.feignClient.SysRoleClient;
+import com.ruoyi.system.api.feignClient.SysUserClient;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.stream.Collectors;
/**
* <p>
@@ -27,11 +54,39 @@
* @author mxcd
* @since 2026-09-22
*/
+@Slf4j
@Service
public class TGroupCompanyServiceImpl extends ServiceImpl<TGroupCompanyMapper, TGroupCompany> implements TGroupCompanyService {
/** 金额保留小数位(与 t_company_recharge_record.arrival_amount 一致) */
private static final int AMOUNT_SCALE = 2;
+
+ /** 公司管理员账号角色标识(内置角色 role_key=company_admin) */
+ private static final String COMPANY_ROLE_KEY = "company_admin";
+
+ /** 公司账号角色类型(sys_user.roleType=5) */
+ private static final int COMPANY_ROLE_TYPE = 5;
+
+ /** 账户模式:共享模式(新增企业未指定时按此建档) */
+ private static final int ACCOUNT_MODE_SHARED = 1;
+
+ /** 账户模式:分配模式 */
+ private static final int ACCOUNT_MODE_ALLOCATION = 2;
+
+ /** 充电用户类型:VIN码 */
+ private static final int USER_TYPE_VIN = 1;
+
+ /** 充电用户类型:刷卡 */
+ private static final int USER_TYPE_CARD = 2;
+
+ /** 充电用户类型:白名单 */
+ private static final int USER_TYPE_WHITELIST = 3;
+
+ /** 账户状态:正常 */
+ private static final int ACCOUNT_STATUS_NORMAL = 1;
+
+ /** 账户状态:欠款 */
+ private static final int ACCOUNT_STATUS_OVERDUE = 2;
/**
* 车队 Mapper。
@@ -44,6 +99,34 @@
/** 公司充值记录 Mapper */
@Resource
private TCompanyRechargeRecordMapper companyRechargeRecordMapper;
+
+ /** 系统用户远程客户端(公司登录账号同步) */
+ @Resource
+ private SysUserClient sysUserClient;
+
+ /** 系统角色远程客户端(公司默认角色查询) */
+ @Resource
+ private SysRoleClient sysRoleClient;
+
+ /** VIN管理远程客户端(公司详情-充电用户信息聚合使用) */
+ @Resource
+ private VinManagementClient vinManagementClient;
+
+ /** 充电卡远程客户端(公司详情-充电用户信息聚合使用) */
+ @Resource
+ private ChargingCardClient chargingCardClient;
+
+ /** 白名单远程客户端(公司详情-充电用户信息聚合使用) */
+ @Resource
+ private AppUserWhitelistClient appUserWhitelistClient;
+
+ /** 企业额度分配记录远程客户端(结算时写消费流水、幂等判定使用) */
+ @Resource
+ private CompanyAllocateRecordClient companyAllocateRecordClient;
+
+ /** 充电订单远程客户端(账户模式切换前进行中订单校验使用) */
+ @Resource
+ private ChargingOrderClient chargingOrderClient;
@Override
public TGroupCompany getDetail(Integer id) {
@@ -61,6 +144,170 @@
// 累计充值到账金额:作为公司总账户余额展示(额度分配弹窗使用),仅覆盖响应对象,不落库
company.setTotalRechargeAmount(sumRechargeAmount(id));
return company;
+ }
+
+ @Override
+ public TGroupCompany getDetailWithChargingUsers(Integer id) {
+ TGroupCompany company = getDetail(id);
+ if (company == null) {
+ return null;
+ }
+ List<TVinManagement> vins = fetchVins(id);
+ // VIN码车辆数量:按公司统计未删除车辆(与 VIN 列表接口同口径),降级时返回 0
+ company.setVinCount(vins.size());
+ company.setChargingUsers(buildChargingUsers(company, vins));
+ return company;
+ }
+
+ /**
+ * 聚合公司充电用户信息:VIN码、刷卡、白名单三类数据按统一结构返回。
+ * 仅公司详情使用;remoteGet 远程调用保持 getDetail(不聚合),避免跨服务循环调用。
+ */
+ private List<CompanyChargingUserVO> buildChargingUsers(TGroupCompany company, List<TVinManagement> vins) {
+ Integer companyId = company.getId();
+ Integer accountMode = company.getAccountMode();
+ List<CompanyChargingUserVO> result = new ArrayList<>();
+
+ List<TChargingCard> cards = fetchCards(companyId);
+ List<TAppUserWhitelist> whitelists = fetchWhitelists(companyId);
+
+ List<Integer> fleetIds = new ArrayList<>();
+ for (TVinManagement vin : vins) {
+ if (vin.getFleetId() != null) {
+ fleetIds.add(vin.getFleetId());
+ }
+ }
+ for (TChargingCard card : cards) {
+ if (card.getFleetId() != null) {
+ fleetIds.add(card.getFleetId());
+ }
+ }
+ for (TAppUserWhitelist whitelist : whitelists) {
+ if (whitelist.getFleetId() != null) {
+ fleetIds.add(whitelist.getFleetId());
+ }
+ }
+ Map<Integer, String> fleetNameMap = loadFleetNameMap(fleetIds);
+
+ String companyName = company.getName();
+ BigDecimal companyBalance = company.getAvailableBalance();
+ for (TVinManagement vin : vins) {
+ result.add(buildRow(USER_TYPE_VIN + "-" + vin.getId(), USER_TYPE_VIN, vin.getVin(),
+ companyName, fleetNameMap.get(vin.getFleetId()), accountMode,
+ vin.getBalance(), companyBalance, vin.getStatus()));
+ }
+ for (TChargingCard card : cards) {
+ result.add(buildRow(USER_TYPE_CARD + "-" + card.getId(), USER_TYPE_CARD, card.getCardNo(),
+ companyName, fleetNameMap.get(card.getFleetId()), accountMode,
+ card.getBalance(), companyBalance, card.getStatus()));
+ }
+ for (TAppUserWhitelist whitelist : whitelists) {
+ result.add(buildRow(USER_TYPE_WHITELIST + "-" + whitelist.getId(), USER_TYPE_WHITELIST, whitelist.getPhone(),
+ companyName, fleetNameMap.get(whitelist.getFleetId()), accountMode,
+ whitelist.getBalance(), companyBalance, whitelist.getStatus()));
+ }
+ return result;
+ }
+
+ /**
+ * 组装单行充电用户信息。可用余额:分配模式取子账户余额/初始分配额度,共享模式取公司可用余额。
+ * 超额/账户状态按余额正负推算:余额为负时超额=|余额|、账户状态=欠款;否则无超额、状态正常。
+ */
+ private CompanyChargingUserVO buildRow(String rowKey, Integer userType, String userInfo,
+ String companyName, String fleetName, Integer accountMode,
+ BigDecimal accountBalance, BigDecimal companyBalance, Integer status) {
+ CompanyChargingUserVO vo = new CompanyChargingUserVO();
+ vo.setRowKey(rowKey);
+ vo.setUserType(userType);
+ vo.setUserInfo(userInfo);
+ vo.setCompanyName(companyName);
+ vo.setFleetName(fleetName);
+ vo.setAccountMode(accountMode);
+ vo.setBalance(resolveBalance(accountMode, accountBalance, companyBalance));
+ vo.setOverrun(null);
+ vo.setAccountStatus(ACCOUNT_STATUS_NORMAL);
+ BigDecimal balance = vo.getBalance();
+ if (balance != null && balance.compareTo(BigDecimal.ZERO) < 0) {
+ vo.setOverrun(balance.abs().setScale(AMOUNT_SCALE, RoundingMode.HALF_UP));
+ vo.setAccountStatus(ACCOUNT_STATUS_OVERDUE);
+ }
+ vo.setStatus(status);
+ return vo;
+ }
+
+ /**
+ * 计算可用余额:分配模式=子账户余额/初始分配额度,共享模式=公司可用余额
+ */
+ private BigDecimal resolveBalance(Integer accountMode, BigDecimal accountBalance, BigDecimal companyBalance) {
+ if (Integer.valueOf(ACCOUNT_MODE_ALLOCATION).equals(accountMode)) {
+ return accountBalance;
+ }
+ if (Integer.valueOf(ACCOUNT_MODE_SHARED).equals(accountMode)) {
+ return companyBalance;
+ }
+ return null;
+ }
+
+ /**
+ * 远程查询公司VIN码列表(降级返回空列表,保证详情主流程不受影响)
+ */
+ private List<TVinManagement> fetchVins(Integer companyId) {
+ try {
+ R<List<TVinManagement>> response = vinManagementClient.listByCompany(companyId);
+ return (response != null && R.isSuccess(response) && response.getData() != null)
+ ? response.getData() : new ArrayList<>();
+ } catch (Exception e) {
+ return new ArrayList<>();
+ }
+ }
+
+ /**
+ * 远程查询公司卡片列表(降级返回空列表)
+ */
+ private List<TChargingCard> fetchCards(Integer companyId) {
+ try {
+ R<List<TChargingCard>> response = chargingCardClient.listByCompany(companyId);
+ return (response != null && R.isSuccess(response) && response.getData() != null)
+ ? response.getData() : new ArrayList<>();
+ } catch (Exception e) {
+ return new ArrayList<>();
+ }
+ }
+
+ /**
+ * 远程查询公司白名单列表(降级返回空列表)
+ */
+ private List<TAppUserWhitelist> fetchWhitelists(Integer companyId) {
+ try {
+ R<List<TAppUserWhitelist>> response = appUserWhitelistClient.listByCompany(companyId);
+ return (response != null && R.isSuccess(response) && response.getData() != null)
+ ? response.getData() : new ArrayList<>();
+ } catch (Exception e) {
+ return new ArrayList<>();
+ }
+ }
+
+ /**
+ * 批量查询车队名称(本地库 t_fleet,逻辑删除由 @TableLogic 自动过滤)
+ */
+ private Map<Integer, String> loadFleetNameMap(List<Integer> fleetIds) {
+ Map<Integer, String> map = new HashMap<>();
+ if (fleetIds == null || fleetIds.isEmpty()) {
+ return map;
+ }
+ List<Integer> distinctIds = fleetIds.stream()
+ .filter(Objects::nonNull)
+ .distinct()
+ .collect(Collectors.toList());
+ if (distinctIds.isEmpty()) {
+ return map;
+ }
+ List<TFleet> fleets = fleetMapper.selectList(
+ Wrappers.<TFleet>lambdaQuery().in(TFleet::getId, distinctIds));
+ for (TFleet fleet : fleets) {
+ map.put(fleet.getId(), fleet.getFleetName());
+ }
+ return map;
}
/**
@@ -116,4 +363,436 @@
.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
}
+ /**
+ * 查询公司默认角色ID(内置角色 role_key=company_admin)
+ */
+ private Long resolveCompanyRoleId() {
+ R<Long> roleR = sysRoleClient.getRoleIdByRoleKey(COMPANY_ROLE_KEY);
+ if (roleR == null || 200 != roleR.getCode() || roleR.getData() == null) {
+ throw new RuntimeException("系统内置角色【" + COMPANY_ROLE_KEY + "】不存在或已停用,请先配置角色");
+ }
+ return roleR.getData();
+ }
+
+ /**
+ * 检查登录账号在 t_group_company 中是否已存在(编辑时排除自身)
+ */
+ private boolean accountExists(String account, Integer excludeId) {
+ Long count = count(Wrappers.<TGroupCompany>lambdaQuery()
+ .eq(TGroupCompany::getAccount, account)
+ .ne(excludeId != null, TGroupCompany::getId, excludeId));
+ return count != null && count > 0;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public AjaxResult addCompany(TGroupCompany company) {
+ if (!StringUtils.hasText(company.getAccount())) {
+ return AjaxResult.error("登录账号不能为空");
+ }
+ // 添加企业弹窗不再配置账户模式:未指定时按共享模式建档,后续可在“账户模式”弹窗切换
+ if (company.getAccountMode() == null) {
+ company.setAccountMode(ACCOUNT_MODE_SHARED);
+ }
+ if (!StringUtils.hasText(company.getPassword())) {
+ return AjaxResult.error("登录密码不能为空");
+ }
+ if (accountExists(company.getAccount(), null)) {
+ return AjaxResult.error("登录账号不能重复");
+ }
+ R<SysUser> existR = sysUserClient.queryUserByUserName(company.getAccount());
+ SysUser existUser = existR == null ? null : existR.getData();
+ if (existUser != null) {
+ return AjaxResult.error("登录账号不能重复");
+ }
+ Long roleId = resolveCompanyRoleId();
+ SysUser user = new SysUser();
+ user.setUserName(company.getAccount());
+ user.setNickName(company.getName());
+ user.setPhonenumber(company.getContactPhone());
+ user.setPassword(company.getPassword());
+ user.setStatus("0");
+ user.setDelFlag("0");
+ user.setRoleId(roleId);
+ user.setRoleType(COMPANY_ROLE_TYPE);
+ user.setPassWordUpdate(new Date());
+ R<Long> r = sysUserClient.addSysUser(user);
+ if (r == null || 200 != r.getCode()) {
+ throw new RuntimeException(r == null ? "创建登录账号失败" : r.getMsg());
+ }
+ save(company);
+ // 回写 objectId=公司ID(账号创建时公司尚未保存)
+ R<SysUser> createdR = sysUserClient.getSysUser(r.getData());
+ if (createdR != null && 200 == createdR.getCode() && createdR.getData() != null) {
+ SysUser created = createdR.getData();
+ created.setObjectId(company.getId());
+ sysUserClient.updateUser(created);
+ }
+ return AjaxResult.success();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public AjaxResult editCompany(TGroupCompany company) {
+ TGroupCompany old = getById(company.getId());
+ if (old == null) {
+ return AjaxResult.error("公司不存在");
+ }
+ // 编辑支持修改登录账号:公司表唯一性校验(排除自身)
+ if (StringUtils.hasText(company.getAccount())) {
+ if (accountExists(company.getAccount(), company.getId())) {
+ return AjaxResult.error("登录账号不能重复");
+ }
+ }
+ String account = StringUtils.hasText(company.getAccount()) ? company.getAccount() : old.getAccount();
+ if (StringUtils.hasText(account)) {
+ String error = syncCompanyUserOnEdit(company, old.getAccount(), account);
+ if (error != null) {
+ return AjaxResult.error(error);
+ }
+ }
+ updateById(company);
+ return AjaxResult.success();
+ }
+
+ /**
+ * 编辑公司时同步登录账号:按旧账号定位用户后更新为新账号(支持改账号);
+ * 密码有值时重置(留空不修改);用户不存在则按新账号补建
+ *
+ * @return 冲突等业务错误信息,null 表示成功
+ */
+ private String syncCompanyUserOnEdit(TGroupCompany company, String oldAccount, String newAccount) {
+ // 账号变更时校验新账号未被其他用户占用(updateSysUser 无唯一性校验,需在此前置拦截)
+ if (StringUtils.hasText(oldAccount) && !newAccount.equals(oldAccount)) {
+ R<SysUser> occupantR = sysUserClient.queryUserByUserName(newAccount);
+ SysUser occupant = occupantR == null ? null : occupantR.getData();
+ if (occupant != null) {
+ R<SysUser> ownerR = sysUserClient.queryUserByUserName(oldAccount);
+ SysUser owner = ownerR == null ? null : ownerR.getData();
+ if (owner == null || !occupant.getUserId().equals(owner.getUserId())) {
+ return "登录账号不能重复";
+ }
+ }
+ }
+ // 按旧账号定位既有用户(账号未变时与新账号一致),避免改名后误走补建导致双账号
+ R<SysUser> userR = sysUserClient.queryUserByUserName(StringUtils.hasText(oldAccount) ? oldAccount : newAccount);
+ SysUser user = userR == null ? null : userR.getData();
+ if (user == null) {
+ if (!StringUtils.hasText(company.getPassword())) {
+ throw new RuntimeException("补建登录账号时登录密码不能为空");
+ }
+ Long roleId = resolveCompanyRoleId();
+ SysUser newUser = new SysUser();
+ newUser.setUserName(newAccount);
+ newUser.setNickName(company.getName());
+ newUser.setPhonenumber(company.getContactPhone());
+ newUser.setPassword(company.getPassword());
+ newUser.setStatus("0");
+ newUser.setDelFlag("0");
+ newUser.setRoleId(roleId);
+ newUser.setRoleType(COMPANY_ROLE_TYPE);
+ newUser.setObjectId(company.getId());
+ newUser.setPassWordUpdate(new Date());
+ R<Long> addR = sysUserClient.addSysUser(newUser);
+ if (addR == null || 200 != addR.getCode()) {
+ throw new RuntimeException(addR == null ? "创建登录账号失败" : addR.getMsg());
+ }
+ return null;
+ }
+ // 远程查询返回的密码字段可能是哈希或脱敏值,显式清空避免再次写回
+ user.setPassword(null);
+ user.setPassWordUpdate(null);
+ user.setUserName(newAccount);
+ user.setNickName(company.getName());
+ user.setPhonenumber(company.getContactPhone());
+ R<Boolean> updateR = sysUserClient.updateSysUser(user);
+ if (updateR == null || 200 != updateR.getCode()) {
+ throw new RuntimeException(updateR == null ? "更新登录账号失败" : updateR.getMsg());
+ }
+ // 密码有值时重置(前端已 HMAC-MD5,由 system 侧 BCrypt 落库,与新增链路一致)
+ if (StringUtils.hasText(company.getPassword())) {
+ SysUser pwdUser = new SysUser();
+ pwdUser.setUserId(user.getUserId());
+ pwdUser.setPassword(company.getPassword());
+ R pwdR = sysUserClient.resetPassword(pwdUser);
+ if (pwdR == null || 200 != pwdR.getCode()) {
+ throw new RuntimeException(pwdR == null ? "重置登录密码失败" : pwdR.getMsg());
+ }
+ }
+ return null;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public AjaxResult delCompany(Integer id) {
+ TGroupCompany company = getById(id);
+ if (company == null) {
+ return AjaxResult.error("公司不存在");
+ }
+ removeById(id);
+ if (StringUtils.hasText(company.getAccount())) {
+ R<SysUser> userR = sysUserClient.queryUserByUserName(company.getAccount());
+ SysUser user = userR == null ? null : userR.getData();
+ if (user != null) {
+ user.setDelFlag("2");
+ sysUserClient.updateUser(user);
+ }
+ }
+ return AjaxResult.success();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public AjaxResult updateAccountMode(Integer id, Integer accountMode) {
+ if (id == null) {
+ return AjaxResult.error("公司ID不能为空");
+ }
+ if (accountMode == null
+ || (accountMode != ACCOUNT_MODE_SHARED && accountMode != ACCOUNT_MODE_ALLOCATION)) {
+ return AjaxResult.error("账户模式不合法");
+ }
+ TGroupCompany company = getById(id);
+ 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)
+ public BigDecimal settleBalance(Integer companyId, BigDecimal lockedAmount, BigDecimal actualAmount, String orderCode,
+ String userInfo, String userName, String remark) {
+ if (companyId == null) {
+ throw new ServiceException("公司ID不能为空");
+ }
+ BigDecimal lock = lockedAmount == null ? BigDecimal.ZERO : lockedAmount;
+ BigDecimal actual = actualAmount == null ? BigDecimal.ZERO : actualAmount;
+ if (lock.compareTo(BigDecimal.ZERO) < 0 || actual.compareTo(BigDecimal.ZERO) < 0) {
+ throw new ServiceException("锁定金额与实际消费金额不能为负数");
+ }
+ boolean hasOrderCode = StringUtils.isNotEmpty(orderCode);
+ // 幂等:账户侧已落该订单消费流水 → 已结算过,直接返回当前余额(结算失败回退订单状态后的补偿重试不会重复结算)
+ if (hasOrderCode && isOrderConsumed(orderCode)) {
+ TGroupCompany existed = getById(companyId);
+ log.info("[公司结算] 订单已结算,幂等跳过:orderCode={}, companyId={}", orderCode, companyId);
+ return existed == null || existed.getAvailableBalance() == null ? BigDecimal.ZERO
+ : existed.getAvailableBalance().setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
+ }
+ boolean updated = lambdaUpdate()
+ .eq(TGroupCompany::getId, companyId)
+ .setSql("locked_amount = GREATEST(IFNULL(locked_amount, 0) - " + lock.toPlainString() + ", 0)"
+ + ", available_balance = IFNULL(available_balance, 0) + " + lock.toPlainString()
+ + " - " + actual.toPlainString())
+ .update();
+ if (!updated) {
+ throw new ServiceException("公司不存在或已删除");
+ }
+ TGroupCompany latest = getById(companyId);
+ BigDecimal after = latest == null || latest.getAvailableBalance() == null ? BigDecimal.ZERO
+ : latest.getAvailableBalance().setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
+ if (hasOrderCode) {
+ // 公司余额在公司库、消费流水在账户库,无法同事务:流水尽力写入,写入成功即作为该订单的结算幂等标记
+ BigDecimal before = after.add(actual).subtract(lock);
+ writeConsumeQuietly(latest, orderCode, userInfo, userName, before, actual.negate(), remark);
+ }
+ return after;
+ }
+
+ /**
+ * 判定账户侧是否已存在该订单的消费流水(查询异常按「未结算」处理,交由订单侧兜底扫描兜住)
+ */
+ private boolean isOrderConsumed(String orderCode) {
+ try {
+ R<List<String>> result = companyAllocateRecordClient.listConsumedOrderCodes(
+ Collections.singletonList(orderCode));
+ return result != null && R.SUCCESS == result.getCode() && result.getData() != null
+ && result.getData().contains(orderCode);
+ } catch (Exception ex) {
+ log.error("[公司结算] 查询消费流水失败,按未结算处理:orderCode={}", orderCode, ex);
+ return false;
+ }
+ }
+
+ /**
+ * 写企业额度分配记录-消费流水(共享模式记企业账户)。失败只记日志,不影响公司余额结算主流程
+ */
+ private void writeConsumeQuietly(TGroupCompany company, String orderCode, String userInfo, String userName,
+ BigDecimal before, BigDecimal amount, String remark) {
+ if (company == null || company.getId() == null) {
+ log.error("[公司结算] 缺少公司信息,无法写消费流水:orderCode={}", orderCode);
+ return;
+ }
+ String userInfoValue = StringUtils.isNotEmpty(userInfo) ? userInfo : company.getName();
+ if (StringUtils.isEmpty(userInfoValue)) {
+ userInfoValue = String.valueOf(company.getId());
+ }
+ String userNameValue = StringUtils.isNotEmpty(userName) ? userName : company.getName();
+ try {
+ R<Boolean> result = companyAllocateRecordClient.recordConsume(company.getId(),
+ TCompanyAllocateRecord.USER_TYPE_COMPANY, userInfoValue, userNameValue, before, amount,
+ orderCode, remark);
+ if (result == null || R.SUCCESS != result.getCode() || !Boolean.TRUE.equals(result.getData())) {
+ log.error("[公司结算] 消费流水写入失败:orderCode={}, companyId={}, msg={}", orderCode, company.getId(),
+ result == null ? "null" : result.getMsg());
+ }
+ } catch (Exception ex) {
+ log.error("[公司结算] 消费流水写入异常:orderCode={}, companyId={}", orderCode, company.getId(), ex);
+ }
+ }
+
}
--
Gitblit v1.7.1