From e03f9a706ba46df72c1914870a0a707d6e5e676b Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期三, 30 九月 2026 09:02:38 +0800
Subject: [PATCH] 修改后台功能

---
 ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml                          |    2 
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java         |  165 +++++++++--
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserWhitelistService.java              |   12 
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistExcelDTO.java              |   58 ++++
 ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java |   84 ++++++
 sql/20260929_whitelist_export_status_auth.sql                                                                  |   57 ++++
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserWhitelistController.java        |   18 +
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardExcelDTO.java                  |   42 ++
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java                       |   20 +
 ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java     |  229 +++++++++++++---
 ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java     |   96 ++++++
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistStatusDTO.java             |   23 +
 ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TAppUserWhitelist.java                   |   16 +
 13 files changed, 745 insertions(+), 77 deletions(-)

diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistExcelDTO.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistExcelDTO.java
new file mode 100644
index 0000000..8ee8c1e
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistExcelDTO.java
@@ -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;
+}
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistStatusDTO.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistStatusDTO.java
new file mode 100644
index 0000000..f78e1a0
--- /dev/null
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistStatusDTO.java
@@ -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;
+}
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardExcelDTO.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardExcelDTO.java
index 71fcf87..002ba1e 100644
--- a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/ChargingCardExcelDTO.java
+++ b/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;
 }
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TAppUserWhitelist.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TAppUserWhitelist.java
index e4eaacb..db89aa8 100644
--- a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TAppUserWhitelist.java
+++ b/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;
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java
index ef34bf5..0b46a07 100644
--- a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TChargingCard.java
+++ b/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;
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserWhitelistController.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserWhitelistController.java
index 8f9b6fa..7a785f2 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserWhitelistController.java
+++ b/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")
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserWhitelistService.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserWhitelistService.java
index 01755cf..1b6b29a 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserWhitelistService.java
+++ b/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);
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
index 68c8bac..0e0ef69 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
+++ b/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<>();
         }
     }
 
diff --git a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java b/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java
index a33d121..dacce8c 100644
--- a/ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TChargingCardServiceImpl.java
+++ b/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;
     }
 
     /**
diff --git a/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml b/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml
index ae2fe49..fcb5732 100644
--- a/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TChargingCardMapper.xml
+++ b/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
diff --git a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java
index 0a27303..b15bec9 100644
--- a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java
+++ b/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);
diff --git a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java
index d573a5f..ed8a040 100644
--- a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TChargingCardServiceImplTest.java
+++ b/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) {
diff --git a/sql/20260929_whitelist_export_status_auth.sql b/sql/20260929_whitelist_export_status_auth.sql
new file mode 100644
index 0000000..f64a1e2
--- /dev/null
+++ b/sql/20260929_whitelist_export_status_auth.sql
@@ -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
+  );

--
Gitblit v1.7.1