puzhibing
2026-09-30 e03f9a706ba46df72c1914870a0a707d6e5e676b
修改后台功能
3个文件已添加
10个文件已修改
822 ■■■■■ 已修改文件
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistExcelDTO.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistStatusDTO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardExcelDTO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TAppUserWhitelist.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserWhitelistController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserWhitelistService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java 229 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260929_whitelist_export_status_auth.sql 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistExcelDTO.java
New file
@@ -0,0 +1,58 @@
package com.ruoyi.account.api.dto;
import com.ruoyi.common.core.annotation.Excel;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
 * 白名单用户 Excel 传输对象(仅用于导出)。
 * <p>公司/车队名称与账户信息(账户模式、可用余额、超额、账户状态)均为导出前远程回填,
 * 其中账户模式=1共享模式时可用余额取所属公司可用余额,=2分配模式时取白名单用户自身余额;
 * 可用余额为负时超额取其绝对值、账户状态记为欠款。</p>
 */
@Data
public class AppUserWhitelistExcelDTO implements Serializable {
    private static final long serialVersionUID = 1L;
    @Excel(name = "用户手机号", sort = 1, width = 16)
    private String phone;
    @Excel(name = "姓名", sort = 2, width = 16)
    private String name;
    @Excel(name = "所属公司", sort = 3, width = 24)
    private String companyName;
    @Excel(name = "所属车队", sort = 4, width = 16)
    private String fleetName;
    /** 账户模式(1=共享模式,2=分配模式) */
    @Excel(name = "账户模式", sort = 5, width = 12, readConverterExp = "1=共享模式,2=分配模式")
    private Integer accountMode;
    /** 可用余额(共享模式=公司可用余额,分配模式=白名单用户自身余额) */
    @Excel(name = "可用余额", sort = 6, width = 14, scale = 2)
    private BigDecimal availableBalance;
    /** 超额(可用余额为负时取其绝对值) */
    @Excel(name = "超额", sort = 7, width = 14, scale = 2)
    private BigDecimal overrun;
    /** 账户状态(1=正常,2=欠款) */
    @Excel(name = "账户状态", sort = 8, width = 12, readConverterExp = "1=正常,2=欠款")
    private Integer accountStatus;
    @Excel(name = "启停状态", sort = 9, width = 12, readConverterExp = "1=启用,2=停用")
    private Integer status;
    /** 所属公司ID(仅导出内部使用,不落Excel) */
    private Integer companyId;
    /** 所属车队ID(仅导出内部使用,不落Excel) */
    private Integer fleetId;
    /** 白名单用户自身余额(分配模式可用余额取值,仅导出内部使用,不落Excel) */
    private BigDecimal balance;
}
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistStatusDTO.java
New file
@@ -0,0 +1,23 @@
package com.ruoyi.account.api.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
 * 白名单用户状态更新参数
 */
@Data
@ApiModel(value = "白名单用户状态更新参数")
public class AppUserWhitelistStatusDTO implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "白名单用户ID", required = true)
    private Long id;
    @ApiModelProperty(value = "状态:1=启用,2=停用", required = true)
    private Integer status;
}
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardExcelDTO.java
@@ -7,18 +7,54 @@
import java.io.Serializable;
import java.math.BigDecimal;
/**
 * 充电卡 Excel 传输对象。
 * <p>导入模板与导入解析只使用「导出导入」列(卡号/卡余额/启用状态);
 * 导出额外使用 type=EXPORT 的派生列(所属公司、所属车队、账户模式、可用余额、超额、账户状态),
 * 派生的公司/车队/余额字段仅供导出内部回填,不参与Excel读写。</p>
 */
@Data
@ApiModel(value = "充电卡导入导出数据")
public class ChargingCardExcelDTO implements Serializable {
    private static final long serialVersionUID = 1L;
    @Excel(name = "卡号", width = 22, prompt = "必填,最多64个字符")
    @Excel(name = "卡号", sort = 1, width = 22, prompt = "必填,最多64个字符")
    private String cardNo;
    @Excel(name = "卡余额", width = 16, scale = 2, prompt = "选填,必须大于等于0")
    @Excel(name = "卡余额", sort = 2, width = 16, scale = 2, prompt = "选填,必须大于等于0")
    private BigDecimal balance;
    @Excel(name = "启用状态", width = 14, readConverterExp = "1=启用,2=禁用", combo = {"启用", "禁用"})
    /** 所属公司(仅导出,按公司ID远程回填公司名称) */
    @Excel(name = "所属公司", sort = 3, width = 24, type = Excel.Type.EXPORT)
    private String companyName;
    /** 所属车队(仅导出,按车队ID远程回填) */
    @Excel(name = "所属车队", sort = 4, width = 16, type = Excel.Type.EXPORT)
    private String fleetName;
    /** 账户模式(仅导出,1=共享模式,2=分配模式) */
    @Excel(name = "账户模式", sort = 5, width = 12, type = Excel.Type.EXPORT, readConverterExp = "1=共享模式,2=分配模式")
    private Integer accountMode;
    /** 可用余额(仅导出,共享模式=公司可用余额,分配模式=卡片自身余额) */
    @Excel(name = "可用余额", sort = 6, width = 14, type = Excel.Type.EXPORT, scale = 2)
    private BigDecimal availableBalance;
    /** 超额(仅导出,可用余额为负时取其绝对值) */
    @Excel(name = "超额", sort = 7, width = 14, type = Excel.Type.EXPORT, scale = 2)
    private BigDecimal overrun;
    /** 账户状态(仅导出,1=正常,2=欠款) */
    @Excel(name = "账户状态", sort = 8, width = 12, type = Excel.Type.EXPORT, readConverterExp = "1=正常,2=欠款")
    private Integer accountStatus;
    @Excel(name = "启用状态", sort = 9, width = 14, readConverterExp = "1=启用,2=禁用", combo = {"启用", "禁用"})
    private String status;
    /** 所属公司ID(仅导出内部使用,不落Excel) */
    private Integer companyId;
    /** 所属车队ID(仅导出内部使用,不落Excel) */
    private Integer fleetId;
}
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TAppUserWhitelist.java
@@ -75,6 +75,22 @@
    @TableField(exist = false)
    private String fleetName;
    @ApiModelProperty(value = "账户模式(列表远程回填,不落库):1=共享模式,2=分配模式")
    @TableField(exist = false)
    private Integer accountMode;
    @ApiModelProperty(value = "可用余额(列表远程回填,不落库):共享模式=公司可用余额,分配模式=白名单用户余额")
    @TableField(exist = false)
    private java.math.BigDecimal availableBalance;
    @ApiModelProperty(value = "超额(列表远程回填,不落库):可用余额为负时取其绝对值")
    @TableField(exist = false)
    private java.math.BigDecimal overrun;
    @ApiModelProperty(value = "账户状态(列表远程回填,不落库):1=正常,2=欠款")
    @TableField(exist = false)
    private Integer accountStatus;
    @ApiModelProperty(value = "状态:1=启用,2=停用")
    @TableField("status")
    private Integer status;
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java
@@ -66,6 +66,26 @@
    @ApiModelProperty(value = "所属车队名称")
    private String fleetName;
    /** 账户模式(列表远程回填,不落库):1=共享模式,2=分配模式 */
    @TableField(exist = false)
    @ApiModelProperty(value = "账户模式:1共享模式,2分配模式")
    private Integer accountMode;
    /** 可用余额(列表远程回填,不落库):分配模式=卡余额,共享模式=公司可用余额 */
    @TableField(exist = false)
    @ApiModelProperty(value = "可用余额:分配模式取卡余额,共享模式取公司可用余额")
    private BigDecimal availableBalance;
    /** 超额(列表远程回填,不落库):可用余额为负时取其绝对值 */
    @TableField(exist = false)
    @ApiModelProperty(value = "超额:可用余额为负时的绝对值")
    private BigDecimal overrun;
    /** 账户状态(列表远程回填,不落库):1=正常,2=欠款 */
    @TableField(exist = false)
    @ApiModelProperty(value = "账户状态:1正常,2欠款")
    private Integer accountStatus;
    @Excel(name = "启用状态", width = 14, readConverterExp = "1=启用,2=禁用")
    @ApiModelProperty(value = "状态:1启用,2禁用")
    private Integer status;
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserWhitelistController.java
@@ -2,9 +2,11 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.api.dto.AppUserWhitelistAllocateDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistExcelDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistImportDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistQueryDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistSaveDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistStatusDTO;
import com.ruoyi.account.api.model.TAppUserWhitelist;
import com.ruoyi.account.api.vo.AppUserWhitelistAllocateVO;
import com.ruoyi.account.api.vo.AppUserWhitelistMatchVO;
@@ -120,6 +122,14 @@
        return R.ok(whitelistService.unbindFleet(ids));
    }
    @ApiOperation("启用或停用白名单用户")
    @RequiresPermissions(value = {"/appUserWhitelist/status"}, logical = Logical.OR)
    @Log(title = "【白名单用户】启用或停用", businessType = BusinessType.UPDATE, operatorType = OperatorType.MANAGE)
    @PutMapping("/status")
    public R<Boolean> status(@RequestBody AppUserWhitelistStatusDTO statusDTO) {
        return R.ok(whitelistService.updateStatus(statusDTO));
    }
    @ApiOperation("额度分配概览(公司总账户余额、可分配余额)")
    @RequiresPermissions(value = {"/appUserWhitelist/allocate"}, logical = Logical.OR)
    @GetMapping("/allocate-overview")
@@ -135,6 +145,14 @@
        return R.ok(whitelistService.allocateQuota(allocateDTO));
    }
    @ApiOperation("导出白名单用户列表")
    @RequiresPermissions(value = {"/appUserWhitelist/export"}, logical = Logical.OR)
    @PutMapping("/export")
    public void export(HttpServletResponse response, @RequestBody(required = false) AppUserWhitelistQueryDTO queryDTO) {
        ExcelUtil<AppUserWhitelistExcelDTO> util = new ExcelUtil<>(AppUserWhitelistExcelDTO.class);
        util.exportExcel(response, whitelistService.selectExportList(queryDTO), "白名单用户数据");
    }
    @ApiOperation("下载白名单用户导入模板")
    @RequiresPermissions(value = {"/appUserWhitelist/import"}, logical = Logical.OR)
    @GetMapping("/importTemplate")
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserWhitelistService.java
@@ -3,8 +3,10 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.account.api.dto.AppUserWhitelistAllocateDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistExcelDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistQueryDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistSaveDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistStatusDTO;
import com.ruoyi.account.api.model.TAppUserWhitelist;
import com.ruoyi.account.api.vo.AppUserWhitelistAllocateVO;
import com.ruoyi.account.api.vo.AppUserWhitelistMatchVO;
@@ -33,6 +35,16 @@
     */
    int unbindFleet(List<Long> ids);
    /**
     * 导出白名单用户列表:与列表查询同一筛选口径(含合作商数据范围),并回填公司与车队名称、账户信息
     */
    List<AppUserWhitelistExcelDTO> selectExportList(AppUserWhitelistQueryDTO queryDTO);
    /**
     * 启用/停用白名单用户:仅允许 1=启用、2=停用,合作商账号仅能操作自身可见的数据
     */
    boolean updateStatus(AppUserWhitelistStatusDTO statusDTO);
    AppUserWhitelistImportResultVO importWhitelist(MultipartFile file) throws Exception;
    AppUserWhitelistMatchVO matchWhitelist(Long appUserId, Integer siteId);
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
@@ -3,12 +3,15 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.api.dto.AppUserWhitelistAllocateDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistAllocateItemDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistExcelDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistImportDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistQueryDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistSaveDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistStatusDTO;
import com.ruoyi.account.api.model.TAppUser;
import com.ruoyi.account.api.model.TAppUserWhitelist;
import com.ruoyi.account.api.model.TAppUserWhitelistSite;
@@ -91,6 +94,15 @@
    /** 姓名最大长度(对应 varchar(64)) */
    private static final int MAX_NAME_LENGTH = 64;
    /** 共享模式(公司账户模式 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;
    /** 金额小数位(元,对应 decimal(12,2)) */
    private static final int AMOUNT_SCALE = 2;
    /** 公司账户模式:2=分配模式(可设置初始分配额度并扣减公司可用余额) */
    private static final int ACCOUNT_MODE_ALLOCATION = 2;
    /** 初始分配额度上限(对应 decimal(12,2)) */
@@ -130,17 +142,28 @@
    @Override
    public Page<TAppUserWhitelist> pageList(AppUserWhitelistQueryDTO queryDTO) {
        if (queryDTO == null) {
            queryDTO = new AppUserWhitelistQueryDTO();
        }
        AppUserWhitelistQueryDTO safeQuery = queryDTO == null ? new AppUserWhitelistQueryDTO() : queryDTO;
        Page<TAppUserWhitelist> page = buildListQuery(safeQuery)
                .orderByDesc(BasePojo::getCreateTime)
                .page(Page.of(safeQuery.getPageCurr(), safeQuery.getPageSize()));
        fillDisplay(page.getRecords());
        return page;
    }
    /**
     * 列表与导出共用的查询条件(含合作商数据范围):条件为空则不参与过滤;
     * 合作商账号只能看到自身合作商录入或本人创建的白名单数据
     */
    private LambdaQueryChainWrapper<TAppUserWhitelist> buildListQuery(AppUserWhitelistQueryDTO queryDTO) {
        AppUserWhitelistQueryDTO safeQuery = queryDTO == null ? new AppUserWhitelistQueryDTO() : queryDTO;
        Integer currentPartnerId = getCurrentPartnerId();
        String username = getUsername();
        LambdaQueryChainWrapper<TAppUserWhitelist> query = this.lambdaQuery()
                .like(StringUtils.isNotBlank(queryDTO.getPhone()), TAppUserWhitelist::getPhone, queryDTO.getPhone())
                .like(StringUtils.isNotBlank(queryDTO.getName()), TAppUserWhitelist::getName, queryDTO.getName())
                .eq(queryDTO.getCompanyId() != null, TAppUserWhitelist::getCompanyId, queryDTO.getCompanyId())
                .eq(queryDTO.getFleetId() != null, TAppUserWhitelist::getFleetId, queryDTO.getFleetId())
                .eq(queryDTO.getStatus() != null, TAppUserWhitelist::getStatus, queryDTO.getStatus())
                .like(StringUtils.isNotBlank(safeQuery.getPhone()), TAppUserWhitelist::getPhone, safeQuery.getPhone())
                .like(StringUtils.isNotBlank(safeQuery.getName()), TAppUserWhitelist::getName, safeQuery.getName())
                .eq(safeQuery.getCompanyId() != null, TAppUserWhitelist::getCompanyId, safeQuery.getCompanyId())
                .eq(safeQuery.getFleetId() != null, TAppUserWhitelist::getFleetId, safeQuery.getFleetId())
                .eq(safeQuery.getStatus() != null, TAppUserWhitelist::getStatus, safeQuery.getStatus())
                .eq(TAppUserWhitelist::getDelFlag, false);
        if (currentPartnerId != null) {
            if (StringUtils.isNotBlank(username)) {
@@ -151,10 +174,77 @@
                query.eq(TAppUserWhitelist::getPartnerId, currentPartnerId);
            }
        }
        Page<TAppUserWhitelist> page = query.orderByDesc(BasePojo::getCreateTime)
                .page(Page.of(queryDTO.getPageCurr(), queryDTO.getPageSize()));
        fillDisplay(page.getRecords());
        return page;
        return query;
    }
    @Override
    public List<AppUserWhitelistExcelDTO> selectExportList(AppUserWhitelistQueryDTO queryDTO) {
        List<TAppUserWhitelist> records = buildListQuery(queryDTO)
                .orderByDesc(BasePojo::getCreateTime)
                .list();
        if (records == null || records.isEmpty()) {
            return Collections.emptyList();
        }
        Map<Integer, String> fleetNameMap = loadFleetNameMap(records.stream()
                .map(TAppUserWhitelist::getFleetId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
        Map<Integer, TGroupCompany> companyMap = loadCompanyMap(records.stream()
                .map(TAppUserWhitelist::getCompanyId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
        List<AppUserWhitelistExcelDTO> items = new ArrayList<>(records.size());
        for (TAppUserWhitelist 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());
            AppUserWhitelistExcelDTO item = new AppUserWhitelistExcelDTO();
            item.setPhone(record.getPhone());
            item.setName(record.getName());
            item.setCompanyName(company == null ? null : company.getName());
            item.setFleetName(fleetNameMap.get(record.getFleetId()));
            item.setAccountMode(accountMode);
            item.setAvailableBalance(balance);
            item.setOverrun(resolveOverrun(balance));
            item.setAccountStatus(resolveAccountStatus(balance));
            item.setStatus(record.getStatus());
            item.setCompanyId(record.getCompanyId());
            item.setFleetId(record.getFleetId());
            item.setBalance(record.getBalance());
            items.add(item);
        }
        return items;
    }
    @Override
    public boolean updateStatus(AppUserWhitelistStatusDTO statusDTO) {
        if (statusDTO == null || statusDTO.getId() == null
                || (!Objects.equals(statusDTO.getStatus(), STATUS_ENABLED)
                && !Objects.equals(statusDTO.getStatus(), STATUS_DISABLED))) {
            throw new ServiceException("白名单用户状态参数错误");
        }
        TAppUserWhitelist existing = this.getById(statusDTO.getId());
        if (existing == null) {
            throw new ServiceException("白名单用户不存在或已删除");
        }
        assertPartnerVisible(existing);
        if (Objects.equals(existing.getStatus(), statusDTO.getStatus())) {
            // 状态未变化视为幂等成功,避免无意义的更新人与更新时间变更
            return true;
        }
        LambdaUpdateChainWrapper<TAppUserWhitelist> updateWrapper = lambdaUpdate()
                .eq(TAppUserWhitelist::getId, statusDTO.getId())
                .set(TAppUserWhitelist::getStatus, statusDTO.getStatus())
                .setSql("update_time = now()");
        String username = getUsername();
        if (StringUtils.isNotBlank(username)) {
            updateWrapper.set(TAppUserWhitelist::getUpdateBy, username);
        }
        if (!updateWrapper.update()) {
            throw new ServiceException("白名单用户状态更新失败,请重试");
        }
        return true;
    }
    @Override
@@ -847,48 +937,102 @@
            record.setSiteList(sites);
            record.setSiteIds(sites.stream().map(TAppUserWhitelistSite::getSiteId).collect(Collectors.toList()));
        }
        fillCompanyNames(records);
        fillCompanyInfo(records);
        fillFleetNames(records);
    }
    /** 列表回填所属公司名称(远程失败仅记录日志,不影响列表查询) */
    private void fillCompanyNames(List<TAppUserWhitelist> records) {
        Map<Integer, String> nameMap = new HashMap<>();
    /**
     * 列表回填所属公司名称与账户信息(账户模式、可用余额、超额、账户状态)。
     * 按公司ID去重后一次批量远程查询,远程失败仅记录日志并留空,不影响列表查询
     */
    private void fillCompanyInfo(List<TAppUserWhitelist> records) {
        Map<Integer, TGroupCompany> companyMap = loadCompanyMap(records.stream()
                .map(TAppUserWhitelist::getCompanyId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
        for (TAppUserWhitelist 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);
            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));
        }
    }
    /** 按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 whitelistBalance, BigDecimal companyBalance) {
        if (Integer.valueOf(ACCOUNT_MODE_ALLOCATION).equals(accountMode)) {
            return whitelistBalance;
        }
        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;
    }
    /** 列表回填所属车队名称(批量查询,远程失败仅记录日志,不影响列表查询) */
    private void fillFleetNames(List<TAppUserWhitelist> records) {
        List<Integer> fleetIds = records.stream()
        Map<Integer, String> nameMap = loadFleetNameMap(records.stream()
                .map(TAppUserWhitelist::getFleetId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (fleetIds.isEmpty()) {
            return;
                .collect(Collectors.toList()));
        records.forEach(record -> record.setFleetName(nameMap.get(record.getFleetId())));
    }
    /** 按ID批量加载车队名称(远程失败返回空Map,由调用方按未知处理) */
    private Map<Integer, String> loadFleetNameMap(List<Integer> fleetIds) {
        List<Integer> distinctIds = fleetIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (distinctIds.isEmpty()) {
            return new HashMap<>();
        }
        try {
            R<List<TFleet>> response = groupCompanyClient.listFleetsByIds(fleetIds);
            R<List<TFleet>> response = groupCompanyClient.listFleetsByIds(distinctIds);
            if (response == null || !R.isSuccess(response) || response.getData() == null) {
                return;
                return new HashMap<>();
            }
            Map<Integer, String> nameMap = new HashMap<>();
            for (TFleet fleet : response.getData()) {
@@ -896,9 +1040,10 @@
                    nameMap.put(fleet.getId(), fleet.getFleetName());
                }
            }
            records.forEach(record -> record.setFleetName(nameMap.get(record.getFleetId())));
            return nameMap;
        } catch (Exception ex) {
            log.warn("回填车队名称失败,fleetIds={}", fleetIds, ex);
            log.warn("回填车队名称失败,fleetIds={}", distinctIds, ex);
            return new HashMap<>();
        }
    }
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java
@@ -71,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");
    /** 额度分配:单次最多分配的卡片数量,防止单次请求过大 */
@@ -103,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;
    }
@@ -384,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
@@ -870,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)
@@ -905,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;
    }
    /**
ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml
@@ -36,6 +36,8 @@
    <select id="selectExportList" resultType="com.ruoyi.account.api.dto.ChargingCardExcelDTO">
        SELECT card_no,
               balance,
               company_id,
               fleet_id,
               status
        FROM t_charging_card
        WHERE del_flag = 0
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java
@@ -6,6 +6,7 @@
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.account.api.dto.AppUserWhitelistAllocateDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistAllocateItemDTO;
import com.ruoyi.account.api.dto.AppUserWhitelistStatusDTO;
import com.ruoyi.account.api.model.TAppUser;
import com.ruoyi.account.api.model.TAppUserWhitelist;
import com.ruoyi.account.api.model.TCompanyAllocateRecord;
@@ -371,6 +372,89 @@
        assertEquals("分配明细中存在重复白名单用户", exception.getMessage());
    }
    // ---------------- updateStatus ----------------
    @Test
    void updateStatusRejectsInvalidParams() {
        assertThrows(ServiceException.class, () -> service.updateStatus(null));
        AppUserWhitelistStatusDTO noId = new AppUserWhitelistStatusDTO();
        noId.setStatus(1);
        assertThrows(ServiceException.class, () -> service.updateStatus(noId));
        AppUserWhitelistStatusDTO badStatus = new AppUserWhitelistStatusDTO();
        badStatus.setId(1L);
        badStatus.setStatus(3);
        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateStatus(badStatus));
        assertEquals("白名单用户状态参数错误", exception.getMessage());
        verify(mapper, never()).update(isNull(), any(Wrapper.class));
    }
    @Test
    void updateStatusRejectsMissingRecord() {
        when(mapper.selectById(1L)).thenReturn(null);
        AppUserWhitelistStatusDTO dto = new AppUserWhitelistStatusDTO();
        dto.setId(1L);
        dto.setStatus(2);
        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateStatus(dto));
        assertEquals("白名单用户不存在或已删除", exception.getMessage());
        verify(mapper, never()).update(isNull(), any(Wrapper.class));
    }
    @Test
    void updateStatusKeepsUnchangedStatusWithoutUpdate() {
        TAppUserWhitelist existing = new TAppUserWhitelist();
        existing.setId(1L);
        existing.setStatus(1);
        when(mapper.selectById(1L)).thenReturn(existing);
        AppUserWhitelistStatusDTO dto = new AppUserWhitelistStatusDTO();
        dto.setId(1L);
        dto.setStatus(1);
        assertTrue(service.updateStatus(dto));
        // 状态未变化时视为幂等成功,不产生更新语句,避免污染 update_time / update_by
        verify(mapper, never()).update(isNull(), any(Wrapper.class));
    }
    @Test
    void updateStatusDisablesWhitelist() {
        TAppUserWhitelist existing = new TAppUserWhitelist();
        existing.setId(1L);
        existing.setStatus(1);
        when(mapper.selectById(1L)).thenReturn(existing);
        when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
        AppUserWhitelistStatusDTO dto = new AppUserWhitelistStatusDTO();
        dto.setId(1L);
        dto.setStatus(2);
        assertTrue(service.updateStatus(dto));
        ArgumentCaptor<Wrapper> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(mapper).update(isNull(), wrapperCaptor.capture());
        String sqlSet = wrapperCaptor.getValue().getSqlSet();
        assertTrue(sqlSet.contains("status"));
        assertTrue(sqlSet.contains("update_time"));
    }
    @Test
    void updateStatusThrowsWhenUpdateAffectsNoRow() {
        TAppUserWhitelist existing = new TAppUserWhitelist();
        existing.setId(1L);
        existing.setStatus(1);
        when(mapper.selectById(1L)).thenReturn(existing);
        when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(0);
        AppUserWhitelistStatusDTO dto = new AppUserWhitelistStatusDTO();
        dto.setId(1L);
        dto.setStatus(2);
        ServiceException exception = assertThrows(ServiceException.class, () -> service.updateStatus(dto));
        assertEquals("白名单用户状态更新失败,请重试", exception.getMessage());
    }
    private TGroupCompany allocationCompany(Integer id, String name) {
        TGroupCompany company = new TGroupCompany();
        company.setId(id);
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java
@@ -42,10 +42,12 @@
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -557,14 +559,102 @@
        Page<TChargingCard> page = new Page<>(1, 10);
        page.setRecords(Collections.singletonList(record));
        when(mapper.selectCardPage(any(Page.class), any())).thenReturn(page);
        when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司")));
        when(groupCompanyClient.listCompaniesByIds(anyList()))
                .thenReturn(R.ok(Collections.singletonList(allocationCompany(9, "测试公司"))));
        when(groupCompanyClient.listFleetsByIds(anyList()))
                .thenReturn(R.ok(Collections.singletonList(fleet(3, 9, "测试车队"))));
        IPage<TChargingCard> result = service.selectCardPage(null);
        assertEquals("测试公司", result.getRecords().get(0).getCompanyName());
        assertEquals("测试车队", result.getRecords().get(0).getFleetName());
        TChargingCard filled = result.getRecords().get(0);
        assertEquals("测试公司", filled.getCompanyName());
        assertEquals("测试车队", filled.getFleetName());
        // 分配模式:可用余额取卡片自身余额,非负为正常、无超额
        assertEquals(2, filled.getAccountMode());
        assertEquals(0, new BigDecimal("10.00").compareTo(filled.getAvailableBalance()));
        assertNull(filled.getOverrun());
        assertEquals(1, filled.getAccountStatus());
    }
    @Test
    void selectCardPageUsesCompanyBalanceForSharedModeCard() {
        TChargingCard record = cardWithQuota(2L, 9, null);
        Page<TChargingCard> page = new Page<>(1, 10);
        page.setRecords(Collections.singletonList(record));
        when(mapper.selectCardPage(any(Page.class), any())).thenReturn(page);
        TGroupCompany shared = new TGroupCompany();
        shared.setId(9);
        shared.setName("共享公司");
        shared.setAccountMode(1);
        shared.setAvailableBalance(new BigDecimal("100.00"));
        when(groupCompanyClient.listCompaniesByIds(anyList()))
                .thenReturn(R.ok(Collections.singletonList(shared)));
        IPage<TChargingCard> result = service.selectCardPage(null);
        TChargingCard filled = result.getRecords().get(0);
        assertEquals(1, filled.getAccountMode());
        assertEquals(0, new BigDecimal("100.00").compareTo(filled.getAvailableBalance()));
        assertNull(filled.getOverrun());
        assertEquals(1, filled.getAccountStatus());
    }
    @Test
    void selectCardPageComputesOverrunForNegativeCardBalance() {
        TChargingCard record = cardWithQuota(3L, 9, new BigDecimal("-5.50"));
        Page<TChargingCard> page = new Page<>(1, 10);
        page.setRecords(Collections.singletonList(record));
        when(mapper.selectCardPage(any(Page.class), any())).thenReturn(page);
        when(groupCompanyClient.listCompaniesByIds(anyList()))
                .thenReturn(R.ok(Collections.singletonList(allocationCompany(9, "分配公司"))));
        IPage<TChargingCard> result = service.selectCardPage(null);
        TChargingCard filled = result.getRecords().get(0);
        assertEquals(0, new BigDecimal("-5.50").compareTo(filled.getAvailableBalance()));
        assertEquals(0, new BigDecimal("5.50").compareTo(filled.getOverrun()));
        assertEquals(2, filled.getAccountStatus());
    }
    @Test
    void selectCardPageLeavesAccountFieldsEmptyWhenCompanyUnavailable() {
        TChargingCard record = cardWithQuota(4L, 9, new BigDecimal("8.00"));
        Page<TChargingCard> page = new Page<>(1, 10);
        page.setRecords(Collections.singletonList(record));
        when(mapper.selectCardPage(any(Page.class), any())).thenReturn(page);
        when(groupCompanyClient.listCompaniesByIds(anyList())).thenReturn(R.fail("远程不可用"));
        IPage<TChargingCard> result = service.selectCardPage(null);
        TChargingCard filled = result.getRecords().get(0);
        assertNull(filled.getCompanyName());
        assertNull(filled.getAccountMode());
        assertNull(filled.getAvailableBalance());
        assertNull(filled.getOverrun());
        assertNull(filled.getAccountStatus());
    }
    @Test
    void selectExportListFillsCompanyFleetAndAccountInfo() {
        ChargingCardExcelDTO item = new ChargingCardExcelDTO();
        item.setCardNo("CARD-9");
        item.setCompanyId(9);
        item.setFleetId(3);
        item.setBalance(new BigDecimal("20.00"));
        when(mapper.selectExportList(any())).thenReturn(Collections.singletonList(item));
        when(groupCompanyClient.listCompaniesByIds(anyList()))
                .thenReturn(R.ok(Collections.singletonList(allocationCompany(9, "测试公司"))));
        when(groupCompanyClient.listFleetsByIds(anyList()))
                .thenReturn(R.ok(Collections.singletonList(fleet(3, 9, "测试车队"))));
        List<ChargingCardExcelDTO> result = service.selectExportList(null);
        assertEquals("测试公司", result.get(0).getCompanyName());
        assertEquals("测试车队", result.get(0).getFleetName());
        assertEquals(2, result.get(0).getAccountMode());
        assertEquals(0, new BigDecimal("20.00").compareTo(result.get(0).getAvailableBalance()));
        assertNull(result.get(0).getOverrun());
        assertEquals(1, result.get(0).getAccountStatus());
    }
    private TGroupCompany allocationCompany(Integer id, String name) {
sql/20260929_whitelist_export_status_auth.sql
New file
@@ -0,0 +1,57 @@
-- 白名单用户管理-导出 / 启停状态切换 按钮权限增量脚本
-- 说明:本功能无表结构变更(复用 t_app_user_whitelist 现有列与远程回填的公司账户信息),
--       仅在 charging_pile_other 逻辑库新增按钮权限。
-- 幂等:已存在同名 perms 时自动跳过,可重复执行。
USE charging_pile_other;
-- 白名单用户管理-导出按钮权限;父菜单按现有白名单用户管理菜单定位(历史数据可能存在多个节点,逐一定位均已覆盖)。
INSERT INTO `sys_menu`
(`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
SELECT permissions.menu_name, parent.menu_id, permissions.order_num, '#', '', '', 1, 0, 'F', '0', '0', permissions.perms, '#', 'admin', NOW(), '', NULL, ''
FROM `sys_menu` parent
JOIN (
    SELECT '导出' AS menu_name, 12 AS order_num, '/appUserWhitelist/export' AS perms
) permissions
WHERE parent.perms = '/appUserWhitelist/list'
  AND NOT EXISTS (
      SELECT 1 FROM `sys_menu` existing WHERE existing.perms = permissions.perms
  );
-- 白名单用户管理-启用/停用按钮权限(列表启停开关)
INSERT INTO `sys_menu`
(`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
SELECT permissions.menu_name, parent.menu_id, permissions.order_num, '#', '', '', 1, 0, 'F', '0', '0', permissions.perms, '#', 'admin', NOW(), '', NULL, ''
FROM `sys_menu` parent
JOIN (
    SELECT '启用/停用' AS menu_name, 13 AS order_num, '/appUserWhitelist/status' AS perms
) permissions
WHERE parent.perms = '/appUserWhitelist/list'
  AND NOT EXISTS (
      SELECT 1 FROM `sys_menu` existing WHERE existing.perms = permissions.perms
  );
-- 默认仅授予管理员角色;其他角色由后台按职责单独授权。
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, child.menu_id
FROM `sys_menu` child
WHERE child.perms = '/appUserWhitelist/export'
  AND child.parent_id IN (
      SELECT parent.menu_id FROM `sys_menu` parent WHERE parent.perms = '/appUserWhitelist/list'
  )
  AND NOT EXISTS (
      SELECT 1 FROM `sys_role_menu` existing
      WHERE existing.role_id = 1 AND existing.menu_id = child.menu_id
  );
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, child.menu_id
FROM `sys_menu` child
WHERE child.perms = '/appUserWhitelist/status'
  AND child.parent_id IN (
      SELECT parent.menu_id FROM `sys_menu` parent WHERE parent.perms = '/appUserWhitelist/list'
  )
  AND NOT EXISTS (
      SELECT 1 FROM `sys_role_menu` existing
      WHERE existing.role_id = 1 AND existing.menu_id = child.menu_id
  );