feat(order): 刷卡充电订单新增所属公司/所属车队筛选与展示,白名单导入补默认状态

【功能变更】
1. 刷卡充电订单新增归属快照列:TCardChargingOrder 新增 companyId/fleetId(company_id/fleet_id),
建单时取充电卡(t_charging_card)当前归属写入订单,充电卡后续改归属不影响历史订单数据。
2. 列表/汇总/导出支持按公司、车队筛选:
- CardChargingOrderQuery 新增 companyId、fleetId 查询条件;
- TCardChargingOrderMapper.xml 新增 o.company_id / o.fleet_id 条件;
- CardChargingOrderExport 导出新增「所属公司」「所属车队」两列。
3. 列表公司/车队名称按订单快照 ID 批量远程取名(GroupCompanyClient.listCompaniesByIds /
listFleetsByIds),公司、车队改名后列表即时生效;远程调用失败仅 warn 跳过回填,
不影响其它字段与接口返回。
4. 白名单导入补默认状态:导入统一按默认值写入(全部免费 + 全部电站 + 启用)且仅校验手机号,
避免 status 为 null 落库后白名单匹配条件(status=启用)不命中。

【测试调整】
TAppUserWhitelistServiceImplTest:断言同步新的导入语义(旧模板的享受权益/白名单电站列被忽略、
统一写入默认值),补充非法手机号错误行覆盖,移除不再触发的 siteClient 打桩。

【数据库脚本(必须先执行 DDL 再发布本代码)】
- sql/20260928_card_charging_order_company_fleet.sql:t_card_charging_order 新增
company_id、fleet_id 两列与 idx_company_id、idx_fleet_id 两个索引;小节A 经代理广播全部分表,
小节B 直连后端实例逐分表幂等执行,小节C 回查校验。
- sql/20260928_card_charging_order_company_fleet_backfill.sql:历史订单按充电卡归属回填,
仅更新 company_id IS NULL 的行,不覆盖上线后新建订单的建单快照。
- 未先执行 DDL 直接发布,刷卡订单列表带公司/车队筛选将报
Column 'company_id' not found in table 'o'(MyCat2/Calcite)。

【影响范围】
t_card_charging_order 表结构;刷卡充电订单列表/汇总/导出;白名单导入路径。
既有查询语义不变,历史订单新增两列默认为 NULL。

【验证】
- mvn -pl ruoyi-api/ruoyi-api-order -am install -DskipTests:BUILD SUCCESS
- mvn -pl ruoyi-service/ruoyi-order,ruoyi-service/ruoyi-account -am test-compile:BUILD SUCCESS
- ruoyi-order surefire 2.22.2:Tests run: 189, Failures: 0, Errors: 0, Skipped: 0
- ruoyi-account surefire 2.22.2:Tests run: 133, Failures: 0, Errors: 0, Skipped: 0
2个文件已添加
8个文件已修改
311 ■■■■■ 已修改文件
doc/耗时统计.md 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/query/CardChargingOrderQuery.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/CardChargingOrderExport.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TCardChargingOrderMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260928_card_charging_order_company_fleet.sql 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20260928_card_charging_order_company_fleet_backfill.sql 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/耗时统计.md
@@ -15,3 +15,5 @@
| 2026-09-21 15:19:30(估算) | 2026-09-21 15:23:29 | 约 240(估算) | 分析代码,汇总下已完成三方对接的品牌:光伏:科士达、华为、隆基、晶科、通威;储能:利普芯储能、华为、宁德时代、阳光电源;监控:海康、萤石、大华;道闸:吉莲、顺捷 | 否(只读分析任务,未涉及代码修改,无需授权口令) | 全项目盘点光伏/储能/视频监控/道闸四类三方对接实现,逐品牌核对真实完成情况,区分「已完成对接 / 仅枚举占位 / 代码中不存在」,并核对数据表品牌字段与同步任务 | `.codebuddy/memory/2026-09-21.md`(新建);`.codebuddy/memory/MEMORY.md`;`doc/耗时统计.md`(未修改任何业务代码) | 只读分析,未执行构建与测试;通过全库关键词检索、类与 SQL 文件走查、调用链核对交叉验证(含 2 个子代理并行探索) | 开始时间因本会话首条命令时间戳不可得而估算;结论:已真实完成对接为科士达、华为(仅光伏)、利普芯(eMind2000)、萤石、通用 HTTP 道闸平台,以及清单外的优旦储能;阳光电源/古瑞瓦特/海康/大华/顺捷、吉联、舜杰、捷联均为枚举占位;隆基、晶科、通威、宁德时代、吉莲在代码中不存在 |
| 2026-09-28(估算,会话首条命令时间不可得) | 2026-09-28(估算) | 约 2400(估算) | 公司管理添加/编辑公司增加登录账号密码字段;定义 roleType=5 为公司账号(先定 3 后改 5) | 是:t_group_company 增加 account/password;内置角色 role_key=company 供公司账号共享;sys_user.roleType 语义统一为 1=平台 2=合作商 3=门店 4=修理厂 5=公司,遗留公司接口 roleType 2→5;公司增/改/删同步创建/更新/停用 sys_user 账号(前端密码 HMAC-MD5) | 公司管理账号密码字段与公司登录账号全链路 | `ruoyi-api/ruoyi-api-other/.../domain/TGroupCompany.java`;`ruoyi-api/ruoyi-api-system/.../feignClient/SysRoleClient.java`;`ruoyi-api/ruoyi-api-system/.../factory/SysRoleFallbackFactory.java`;`ruoyi-modules/ruoyi-system/.../controller/SysRoleController.java`;`ruoyi-modules/ruoyi-system/.../controller/SysUserController.java`;`ruoyi-modules/ruoyi-system/src/test/.../PartnerTypeRoleServiceTest.java`;`ruoyi-service/ruoyi-other/.../service/TGroupCompanyService.java`;`ruoyi-service/ruoyi-other/.../service/impl/TGroupCompanyServiceImpl.java`;`ruoyi-service/ruoyi-other/.../controller/TGroupCompanyController.java`;`ruoyi-service/ruoyi-other/src/test/.../TGroupCompanyServiceImplTest.java`;`ruoyi-service/ruoyi-other/src/test/.../EmindEnergyChainManagementServiceTest.java`;`sql/20260928_company_account_role.sql`;`qianduan/chargingPile_management/src/pages/company/index.jsx`;`doc/耗时统计.md` | 后端 4 模块 `mvn -am compile` 通过;surefire 2.22.2 跑 TGroupCompanyServiceImplTest 6/6、PartnerTypeRoleServiceTest 7/7 全部通过;前端 `tsc --noEmit` 通过;前端生产构建成功(dist 生成);IDE lint 0 条 | SQL 脚本未在数据库执行,待部署时执行;修复了 2 处历史遗留测试(EmindEnergyChainManagementServiceTest 构造参数不同步、TGroupCompanyServiceImplTest 断言与 getDetail 双查询不一致);前端构建需在脚本内设置 CODEBUDDY_SAFE_DELETE_ENABLED=0 且勿删除被占用日志文件 |
| 2026-09-28 14:27:21(估算,取授权后首个产出文件创建时间) | 2026-09-28 14:45:50 | 约 1109(估算,含测试、构建与收尾归档) | VIN码充电订单增加所属公司、所属车队:需公司与车队联动、按方案B实现、VIN 管理页也增加筛选条件 | 是:先输出方案A(按 VIN 集合筛选)与方案B(订单表冗余归属列、SQL 直接筛选)对比,用户确认方案B与联动后输出修订版方案并获「开始执行」授权 | 订单表新增 company_id、fleet_id 快照列(建单时取 VIN 管理记录归属);列表/汇总/导出共用 companyId、fleetId 筛选;按 ID 批量远程取名回填所属公司/车队;VIN 管理页导出条件与列表对齐;两页搜索区公司与车队联动下拉 | ruoyi-api/ruoyi-api-order/.../api/model/TVinChargingOrder.java、ruoyi-api/ruoyi-api-order/.../api/query/VinChargingOrderQuery.java、ruoyi-api/ruoyi-api-order/.../api/vo/ChargingOrderVO.java、ruoyi-api/ruoyi-api-other/.../api/feignClient/GroupCompanyClient.java、ruoyi-api/ruoyi-api-other/.../api/factory/GroupCompanyFallbackFactory.java、ruoyi-service/ruoyi-order/.../service/impl/TVinChargingOrderServiceImpl.java、ruoyi-service/ruoyi-order/.../export/VinChargingOrderExport.java、ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TVinChargingOrderMapper.xml、ruoyi-service/ruoyi-order/src/test/.../mapper/VinChargingOrderMapperStructureTest.java、ruoyi-service/ruoyi-order/src/test/.../impl/TVinChargingOrderCompanyFleetTest.java(新增)、ruoyi-service/ruoyi-other/.../controller/TGroupCompanyController.java、ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TVinManagementMapper.xml、ruoyi-service/ruoyi-account/src/test/.../mapper/VinManagementMapperStructureTest.java(新增)、sql/20260928_vin_charging_order_company_fleet.sql(新增)、sql/20260928_vin_charging_order_company_fleet_backfill.sql(新增)、qianduan/chargingPile_management/src/components/FleetSearchSelect/(新增 index.jsx、fleetOptions.js、fleetOptions.test.js)、qianduan/chargingPile_management/src/pages/order/vinChargingOrder/index.jsx、service.js、service.test.js、qianduan/chargingPile_management/src/pages/vin/index.jsx | 后端 surefire 2.22.2 定向执行 ruoyi-order 四个测试类(TVinChargingOrderCompanyFleetTest 4、VinChargingOrderMapperStructureTest 1、TVinChargingOrderVinBackfillTest 5、TVinChargingOrderHandshakeTest 7)与 ruoyi-account VinManagementMapperStructureTest 1,全部通过;前端 npx jest 2 套件 5 用例通过;前端 npm run build 成功(BUILD_EXIT=0,dist 生成);IDE lint 0 错误 | SQL 未在数据库执行:分片表 DDL 需经 MyCat 代理或直连后端 MySQL 逐分表执行,历史订单回填脚本需人工生成语句并执行;建单快照语义为「建单当时归属」,车辆改归属不影响历史订单;导出 Excel 新增所属公司、所属车队两列;订单页列缓存 persistenceKey 需重置列设置;方案修订阶段(授权前)耗时未记录,开始时间取授权后首个产出文件创建时间;构建日志归档 logs/build-20260928-vinOrderCompanyFleet.log |
| 2026-09-28 15:05:00(估算,会话时间戳不可得) | 2026-09-28 15:35:00(估算) | 约 1800(估算,含方案输出、三轮确认问答与实施测试) | 白名单添加页面增加姓名、状态字段,删除享受权益、白名单电站 | 是:姓名选填、状态启用/停用(新增默认启用)、列表页同步增删列并新增姓名/公司/车队/状态筛选、导入模板同步删除两列(导入默认全部免费+全部电站),经三轮确认问答后获「开始执行」授权 | 白名单表新增 name 列;保存校验姓名长度/状态合法并归一化,benefitType/scopeType 不传默认 1(传了仍校验);新增/编辑保存姓名与状态(编辑不再强制置启用);列表 pageList 支持 name/companyId/fleetId/status 筛选;导入模板仅保留手机号列、表头判断只校验第 0 列、导入行校验与 DTO 构建简化;前端弹窗增姓名/状态、删除享受权益/白名单电站及指定站点/合作商联动;列表增姓名/状态列并删除白名单电站/享受权益列 | sql/20260928_app_user_whitelist_name_status.sql(新增);ruoyi-api/ruoyi-api-account/.../model/TAppUserWhitelist.java;ruoyi-api/ruoyi-api-account/.../dto/AppUserWhitelistSaveDTO.java、AppUserWhitelistQueryDTO.java、AppUserWhitelistImportDTO.java;ruoyi-service/ruoyi-account/.../controller/TAppUserWhitelistController.java;ruoyi-service/ruoyi-account/.../service/impl/TAppUserWhitelistServiceImpl.java;ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TAppUserWhitelistMapper.xml;qianduan/chargingPile_management/src/pages/person/whitelist/index.jsx、service.js、components/AddAndEdit.jsx、components/ImportModal.jsx、components/SiteSelectModal.jsx(删除) | 后端 `mvn -pl ":ruoyi-modules-account" -am compile` 通过;前端 `npm run tsc` 通过;IDE lint 0 条;新增/编辑/列表筛选/导入全链路代码走查通过 | SQL 脚本未在数据库执行,待部署时执行;导入路径绕过 validateSaveDTO,走查发现 buildSaveDTO 需补充默认 status=1 防止 null 落库导致匹配失效,已修复;编辑历史白名单统一落库全部电站+全部免费并清除旧站点关联;停用状态不命中匹配(既有 status=启用 条件);删除已无引用的 SiteSelectModal.jsx |
| 2026-09-28 14:51:55(估算,取本次首个产出文件创建时间) | 2026-09-28 14:54:44 | 约 170(估算,仅本轮执行阶段) | 报错修复:查询 VIN 码充电订单报 `Column 'company_id' not found in table 'o'`(MyCat2/Calcite),订单列表携带公司与车队筛选时 500 | 是:定位为订单表未执行本次功能 DDL(仅代码上线),方案为经 MyCat 代理执行建列建索引 DDL + 历史订单回填 + SQL 级回归;方案已输出并获「开始执行」授权 | 在 charging_pile_order.t_vin_charging_order 上新增 company_id、fleet_id 两列与 idx_company_id、idx_fleet_id 两个索引(经 MyCat 8066 广播至 20 张分表);复跑原报错 SQL 形态验证不再抛 Calcite 异常;按回填脚本逻辑生成并执行历史订单归属 UPDATE;EXPLAIN 校验筛选条件下推与分表覆盖;编译订单服务 | sql/20260928_vin_charging_order_company_fleet.sql(按脚本执行 DDL,文件未修改)、sql/20260928_vin_charging_order_company_fleet_backfill.sql(按其逻辑生成语句执行)、doc/耗时统计.md;业务代码零改动 | 后端 mvn -o -pl ruoyi-service/ruoyi-order -am compile BUILD SUCCESS;DESC / SHOW CREATE TABLE / information_schema 回查两列两索引齐全、中文注释无乱码、仍为 TBPARTITIONS 20;原报错 COUNT SQL 复跑正常;pageList 形态查询返回 company_id=1、fleet_id=4;汇总 SQL 聚合正常;company 单独与 fleet 单独筛选各命中 1 条;VIN 管理筛选命中 1 条;EXPLAIN 显示条件已下推 t_vin_charging_order_0..19;IDE lint 0 错误 | 仅修改测试库(192.168.110.111:8066,MyCat 2.0 支持经代理 ALTER 广播分表);报错机器 C:\Users\PC\project\charging\mx_charging_pile 若连的是另一套库,需同样执行该 DDL 脚本;历史回填仅 1 条订单(company_id=1、fleet_id=4);未分配归属的订单两列保持 NULL;开始时间取本轮首个产出文件创建时间,上一轮方案分析耗时未计 |
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TCardChargingOrder.java
@@ -20,4 +20,12 @@
    @ApiModelProperty("充电卡号快照")
    @TableField("card_no")
    private String cardNo;
    @ApiModelProperty("所属公司ID(建单时充电卡快照,关联t_group_company.id)")
    @TableField("company_id")
    private Integer companyId;
    @ApiModelProperty("所属车队ID(建单时充电卡快照,关联t_fleet.id)")
    @TableField("fleet_id")
    private Integer fleetId;
}
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/query/CardChargingOrderQuery.java
@@ -32,6 +32,12 @@
    @ApiModelProperty("充电枪ID")
    private Integer gunId;
    @ApiModelProperty("所属公司ID")
    private Integer companyId;
    @ApiModelProperty("所属车队ID")
    private Integer fleetId;
    @ApiModelProperty("充电开始时间范围的开始时间")
    private String startTime1;
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java
@@ -578,13 +578,14 @@
        return errors;
    }
    /** 导入数据统一按默认值处理:全部免费 + 全部电站 */
    /** 导入数据统一按默认值处理:全部免费 + 全部电站 + 启用 */
    private AppUserWhitelistSaveDTO buildSaveDTO(AppUserWhitelistImportDTO row) {
        AppUserWhitelistSaveDTO dto = new AppUserWhitelistSaveDTO();
        dto.setPhone(normalize(row.getPhone()));
        dto.setBenefitType(BENEFIT_ALL_FREE);
        dto.setScopeType(SCOPE_ALL_SITE);
        dto.setSiteIds(Collections.emptyList());
        dto.setStatus(STATUS_ENABLED);
        return dto;
    }
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java
@@ -9,7 +9,6 @@
import com.ruoyi.account.service.TAppUserWhitelistSiteService;
import com.ruoyi.chargingPile.api.feignClient.PartnerClient;
import com.ruoyi.chargingPile.api.feignClient.SiteClient;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.other.api.feignClient.GroupCompanyClient;
import com.ruoyi.system.api.feignClient.SysUserClient;
@@ -27,7 +26,6 @@
import org.springframework.transaction.TransactionStatus;
import java.io.ByteArrayOutputStream;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@@ -78,7 +76,6 @@
    @Test
    void importsValidRowsAndReturnsOnlyInvalidRows() throws Exception {
        when(siteClient.getSiteAll()).thenReturn(R.ok(Collections.emptyList()));
        when(mapper.selectValidByPhone(any())).thenReturn(null);
        TransactionStatus transactionStatus = mock(TransactionStatus.class);
        when(transactionManager.getTransaction(any())).thenReturn(transactionStatus);
@@ -96,11 +93,18 @@
        AppUserWhitelistImportResultVO result = service.importWhitelist(importFile());
        assertEquals(3, result.getTotalCount());
        assertEquals(2, result.getSuccessCount());
        assertEquals(4, result.getTotalCount());
        assertEquals(3, result.getSuccessCount());
        assertEquals(1, result.getErrorCount());
        assertEquals("13540211237", result.getErrorMessage().get(0).getPhone());
        verify(mapper, org.mockito.Mockito.times(2)).insert(any(TAppUserWhitelist.class));
        assertEquals("1234", result.getErrorMessage().get(0).getPhone());
        org.mockito.ArgumentCaptor<TAppUserWhitelist> captor = org.mockito.ArgumentCaptor.forClass(TAppUserWhitelist.class);
        verify(mapper, org.mockito.Mockito.times(3)).insert(captor.capture());
        for (TAppUserWhitelist saved : captor.getAllValues()) {
            // 导入统一按默认值写入:全部免费 + 全部电站 + 启用,忽略模板中的享受权益/白名单电站列
            assertEquals(1, saved.getBenefitType().intValue());
            assertEquals(1, saved.getScopeType().intValue());
            assertEquals(1, saved.getStatus().intValue());
        }
    }
    private MockMultipartFile importFile() throws Exception {
@@ -113,6 +117,7 @@
            addRow(sheet, 1, "13540211236", "1", "all");
            addRow(sheet, 2, "13540211238", "2", "all");
            addRow(sheet, 3, "13540211237", "", "all");
            addRow(sheet, 4, "1234", "", "all");
            workbook.write(output);
            return new MockMultipartFile("file", "whitelist.xlsx",
                    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", output.toByteArray());
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/CardChargingOrderExport.java
@@ -19,6 +19,12 @@
    @Excel(name = "刷卡开卡号", width = 20)
    private String cardNo;
    @Excel(name = "所属公司", width = 24)
    private String companyName;
    @Excel(name = "所属车队", width = 20)
    private String fleetName;
    @Excel(name = "站点名称", width = 24)
    private String siteName;
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TCardChargingOrderServiceImpl.java
@@ -51,6 +51,9 @@
import com.ruoyi.integration.api.vo.NotificationConnectorChargeStatusVo;
import com.ruoyi.order.util.TransactionStageParser;
import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService;
import com.ruoyi.other.api.domain.TFleet;
import com.ruoyi.other.api.domain.TGroupCompany;
import com.ruoyi.other.api.feignClient.GroupCompanyClient;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
@@ -135,6 +138,8 @@
    private UploadRealTimeMonitoringDataService uploadRealTimeMonitoringDataService;
    @Resource
    private ChargingCardClient chargingCardClient;
    @Resource
    private GroupCompanyClient groupCompanyClient;
    @Resource
    private AccountingStrategyClient accountingStrategyClient;
    @Resource
@@ -453,6 +458,7 @@
            return;
        }
        fillElectronicProportion(list);
        fillCompanyFleetNames(list);
        List<Integer> siteIds = list.stream().map(ChargingOrderVO::getSiteId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        List<Integer> gunIds = list.stream().map(ChargingOrderVO::getChargingGunId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        List<Long> carIds = list.stream().map(ChargingOrderVO::getAppUserCarId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
@@ -484,6 +490,73 @@
            item.setPlatformIncome(BigDecimal.ZERO);
            item.setPartnerIncome(BigDecimal.ZERO);
        }
    }
    /**
     * 回填所属公司/车队名称:按订单快照 companyId/fleetId 批量远程取名(公司/车队改名后列表即时生效),
     * 远程调用失败仅跳过回填,不影响列表其他字段。
     */
    private void fillCompanyFleetNames(List<ChargingOrderVO> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Integer> companyIds = list.stream().map(ChargingOrderVO::getCompanyId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (!companyIds.isEmpty()) {
            Map<Integer, String> companyNames = loadCompanyNames(companyIds);
            for (ChargingOrderVO item : list) {
                String name = item.getCompanyId() == null ? null : companyNames.get(item.getCompanyId());
                if (StringUtils.isNotEmpty(name)) {
                    item.setCompanyName(name);
                }
            }
        }
        List<Integer> fleetIds = list.stream().map(ChargingOrderVO::getFleetId)
                .filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (fleetIds.isEmpty()) {
            return;
        }
        Map<Integer, String> fleetNames = loadFleetNames(fleetIds);
        for (ChargingOrderVO item : list) {
            String name = item.getFleetId() == null ? null : fleetNames.get(item.getFleetId());
            if (StringUtils.isNotEmpty(name)) {
                item.setFleetName(name);
            }
        }
    }
    /** 批量查询公司名称,失败返回已取到的数据 */
    private Map<Integer, String> loadCompanyNames(List<Integer> companyIds) {
        Map<Integer, String> result = new HashMap<>();
        try {
            List<TGroupCompany> companies = Optional.ofNullable(groupCompanyClient.listCompaniesByIds(companyIds).getData())
                    .orElse(Collections.emptyList());
            for (TGroupCompany company : companies) {
                if (company != null && company.getId() != null && StringUtils.isNotEmpty(company.getName())) {
                    result.put(company.getId(), company.getName());
                }
            }
        } catch (Exception e) {
            log.warn("[SD回填] 批量查询公司名称失败 ids={}", companyIds, e);
        }
        return result;
    }
    /** 批量查询车队名称,失败返回已取到的数据 */
    private Map<Integer, String> loadFleetNames(List<Integer> fleetIds) {
        Map<Integer, String> result = new HashMap<>();
        try {
            List<TFleet> fleets = Optional.ofNullable(groupCompanyClient.listFleetsByIds(fleetIds).getData())
                    .orElse(Collections.emptyList());
            for (TFleet fleet : fleets) {
                if (fleet != null && fleet.getId() != null && StringUtils.isNotEmpty(fleet.getFleetName())) {
                    result.put(fleet.getId(), fleet.getFleetName());
                }
            }
        } catch (Exception e) {
            log.warn("[SD回填] 批量查询车队名称失败 ids={}", fleetIds, e);
        }
        return result;
    }
    private String[] parseTimeRange(CardChargingOrderQuery query) {
@@ -559,6 +632,9 @@
                order.setChargingGunId(gun.getId());
                order.setCardId(card.getId());
                order.setCardNo(card.getCardNo());
                // 所属公司/车队快照落库(充电卡当前归属),供订单列表/汇总/导出按公司、车队直接筛选
                order.setCompanyId(card.getCompanyId());
                order.setFleetId(card.getFleetId());
                // 可用金额基数与启动剩余金额(与返回设备的 balance 同源,供监控/结算扣减与前端展示)
                order.setChargeAmount(card.getBalance());
                order.setResidualAmount(card.getBalance());
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TCardChargingOrderMapper.xml
@@ -17,6 +17,12 @@
            <if test="query.cardNo != null and query.cardNo != ''">
                AND o.card_no LIKE CONCAT('%', #{query.cardNo}, '%')
            </if>
            <if test="query.companyId != null">
                AND o.company_id = #{query.companyId}
            </if>
            <if test="query.fleetId != null">
                AND o.fleet_id = #{query.fleetId}
            </if>
            <if test="query.status != null">
                AND o.status = #{query.status}
            </if>
sql/20260928_card_charging_order_company_fleet.sql
New file
@@ -0,0 +1,129 @@
-- 刷卡充电订单新增「所属公司 / 所属车队」快照列
-- 目的:刷卡充电订单列表/汇总/导出支持按公司、车队筛选与展示(订单表冗余归属列 + SQL 直接筛选)
-- 目标库:charging_pile_order(表 t_card_charging_order,TBPARTITION BY MOD_HASH(card_id) TBPARTITIONS 20)
-- 数据来源:建单时取 t_charging_card(充电卡)的 company_id / fleet_id 快照;
--           历史订单回填见同目录 20260928_card_charging_order_company_fleet_backfill.sql
-- 执行方式:
--   1) 首选「小节 A」经数据库代理(MyCat 8066)执行,代理会把 DDL 广播到全部物理分表;
--   2) 代理不支持分片表 DDL 时,改用「小节 B」直连后端 MySQL 实例执行(存储过程,逐物理分表,幂等);
--   3) 最后执行「小节 C」回查校验。
-- 幂等说明:小节 A 为裸 ALTER,重复执行会报 Duplicate column,忽略即可(以小节 C 回查为准);
--           小节 B 先查 information_schema 再变更,可重复执行。
-- 部署提示:必须先执行本脚本再发布订单服务代码;否则列表带公司/车队筛选会报 Column 'company_id' not found。
-- ============ 小节 A:经代理执行(推荐) ============
USE `charging_pile_order`;
ALTER TABLE `t_card_charging_order`
  ADD COLUMN `company_id` int DEFAULT NULL COMMENT '所属公司ID(建单时充电卡快照,关联t_group_company.id)';
ALTER TABLE `t_card_charging_order`
  ADD COLUMN `fleet_id` int DEFAULT NULL COMMENT '所属车队ID(建单时充电卡快照,关联t_fleet.id)';
ALTER TABLE `t_card_charging_order` ADD KEY `idx_company_id` (`company_id`);
ALTER TABLE `t_card_charging_order` ADD KEY `idx_fleet_id` (`fleet_id`);
-- ============ 小节 B:直连后端 MySQL 实例执行(代理不可用时分表执行,幂等) ============
USE `charging_pile_order`;
DROP PROCEDURE IF EXISTS `add_card_charging_order_company_fleet`;
DELIMITER $$
CREATE PROCEDURE `add_card_charging_order_company_fleet`()
BEGIN
    DECLARE done INT DEFAULT 0;
    DECLARE target_table VARCHAR(64);
    DECLARE column_exists INT DEFAULT 0;
    DECLARE index_exists INT DEFAULT 0;
    -- 覆盖逻辑表(若物理存在)与 TBPARTITION 物理分表 t_card_charging_order_0..N
    DECLARE table_cursor CURSOR FOR
        SELECT t.table_name
        FROM information_schema.tables t
        WHERE t.table_schema = DATABASE()
          AND t.table_name REGEXP '^t_card_charging_order(_[0-9]+)?$';
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
    OPEN table_cursor;
    table_loop: LOOP
        FETCH table_cursor INTO target_table;
        IF done = 1 THEN
            LEAVE table_loop;
        END IF;
        SELECT COUNT(*) INTO column_exists
        FROM information_schema.columns
        WHERE table_schema = DATABASE()
          AND table_name = target_table
          AND column_name = 'company_id';
        IF column_exists = 0 THEN
            SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `company_id` int DEFAULT NULL COMMENT ''所属公司ID(建单时充电卡快照,关联t_group_company.id)''');
            PREPARE stmt FROM @ddl;
            EXECUTE stmt;
            DEALLOCATE PREPARE stmt;
        END IF;
        SELECT COUNT(*) INTO column_exists
        FROM information_schema.columns
        WHERE table_schema = DATABASE()
          AND table_name = target_table
          AND column_name = 'fleet_id';
        IF column_exists = 0 THEN
            SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `fleet_id` int DEFAULT NULL COMMENT ''所属车队ID(建单时充电卡快照,关联t_fleet.id)''');
            PREPARE stmt FROM @ddl;
            EXECUTE stmt;
            DEALLOCATE PREPARE stmt;
        END IF;
        SELECT COUNT(*) INTO index_exists
        FROM information_schema.statistics
        WHERE table_schema = DATABASE()
          AND table_name = target_table
          AND index_name = 'idx_company_id';
        IF index_exists = 0 THEN
            SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD KEY `idx_company_id` (`company_id`)');
            PREPARE stmt FROM @ddl;
            EXECUTE stmt;
            DEALLOCATE PREPARE stmt;
        END IF;
        SELECT COUNT(*) INTO index_exists
        FROM information_schema.statistics
        WHERE table_schema = DATABASE()
          AND table_name = target_table
          AND index_name = 'idx_fleet_id';
        IF index_exists = 0 THEN
            SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD KEY `idx_fleet_id` (`fleet_id`)');
            PREPARE stmt FROM @ddl;
            EXECUTE stmt;
            DEALLOCATE PREPARE stmt;
        END IF;
    END LOOP;
    CLOSE table_cursor;
END$$
DELIMITER ;
CALL `add_card_charging_order_company_fleet`();
DROP PROCEDURE `add_card_charging_order_company_fleet`;
-- ============ 小节 C:回查校验(应看到每个物理分表都存在 company_id / fleet_id 两列) ============
SELECT t.table_name, c.column_name, c.column_type, c.column_comment
FROM information_schema.tables t
JOIN information_schema.columns c
  ON c.table_schema = t.table_schema
 AND c.table_name = t.table_name
WHERE t.table_schema = DATABASE()
  AND t.table_name REGEXP '^t_card_charging_order(_[0-9]+)?$'
  AND c.column_name IN ('company_id', 'fleet_id')
ORDER BY t.table_name, c.column_name;
SELECT COUNT(DISTINCT t.table_name) AS shard_count,
       SUM(CASE WHEN c.column_name = 'company_id' THEN 1 ELSE 0 END) AS company_id_columns,
       SUM(CASE WHEN c.column_name = 'fleet_id' THEN 1 ELSE 0 END) AS fleet_id_columns
FROM information_schema.tables t
JOIN information_schema.columns c
  ON c.table_schema = t.table_schema
 AND c.table_name = t.table_name
WHERE t.table_schema = DATABASE()
  AND t.table_name REGEXP '^t_card_charging_order(_[0-9]+)?$'
  AND c.column_name IN ('company_id', 'fleet_id');
sql/20260928_card_charging_order_company_fleet_backfill.sql
New file
@@ -0,0 +1,56 @@
-- 刷卡充电订单「所属公司 / 所属车队」历史数据回填脚本
-- 前置:已执行 20260928_card_charging_order_company_fleet.sql(两列已存在)
-- 数据来源:charging_pile_account.t_charging_card(含 company_id / fleet_id / del_flag)
-- 执行方式:MyCat 不支持跨逻辑库 UPDATE JOIN,故分三步人工执行:
--   步骤1 在 charging_pile_account 生成 UPDATE 语句(单库只读查询);
--   步骤2 把步骤1 结果集逐行复制到 charging_pile_order 执行(card_id 为分片键,可路由单分片);
--   步骤3 回查校验。
-- 语义与幂等:
--   * 仅回填 company_id IS NULL 的历史订单行,不覆盖上线后新建订单的建单时快照;
--   * 同一卡重复执行结果一致(写入充电卡当前归属);
--   * 卡未分配公司且未分配车队时不生成语句,其历史订单两列保持 NULL。
-- ============ 步骤 0:前置检查(charging_pile_account,只读) ============
USE `charging_pile_account`;
SELECT COUNT(1) AS assignable_card_count
FROM `t_charging_card`
WHERE `del_flag` = 0 AND (`company_id` IS NOT NULL OR `fleet_id` IS NOT NULL);
SELECT COUNT(1) AS unassigned_card_count
FROM `t_charging_card`
WHERE `del_flag` = 0 AND `company_id` IS NULL AND `fleet_id` IS NULL;
-- ============ 步骤 1:生成回填语句(charging_pile_account,只读) ============
-- 复制结果集 backfill_sql 列全部内容,粘贴到 charging_pile_order 逐行执行
SELECT CONCAT('UPDATE `t_card_charging_order` SET `company_id` = ', IFNULL(c.`company_id`, 'NULL'),
              ', `fleet_id` = ', IFNULL(c.`fleet_id`, 'NULL'),
              ' WHERE `card_id` = ', c.`id`, ' AND `company_id` IS NULL;') AS backfill_sql
FROM `t_charging_card` c
WHERE c.`del_flag` = 0
  AND (c.`company_id` IS NOT NULL OR c.`fleet_id` IS NOT NULL)
ORDER BY c.`id`;
-- ============ 步骤 2:在 charging_pile_order 执行步骤1 生成的语句 ============
-- 示例(card_id = 1 的卡归属公司 12、车队 3):
-- USE `charging_pile_order`;
-- UPDATE `t_card_charging_order` SET `company_id` = 12, `fleet_id` = 3 WHERE `card_id` = 1 AND `company_id` IS NULL;
-- ============ 步骤 3:回查校验(charging_pile_order) ============
USE `charging_pile_order`;
-- 全量盘点:总数 / 已回填公司 / 已回填车队 / 两者皆空
SELECT COUNT(1) AS order_total,
       SUM(CASE WHEN `company_id` IS NOT NULL THEN 1 ELSE 0 END) AS company_filled,
       SUM(CASE WHEN `fleet_id` IS NOT NULL THEN 1 ELSE 0 END) AS fleet_filled,
       SUM(CASE WHEN `company_id` IS NULL AND `fleet_id` IS NULL THEN 1 ELSE 0 END) AS unassigned
FROM `t_card_charging_order`;
-- 以下跨逻辑库比对请【直连后端 MySQL 实例】执行(代理不支持跨库 JOIN):
-- 归属公司与充电卡不一致的订单(回填后应为 0;上线后新建订单因卡改归属而不为 0 属预期)
-- SELECT COUNT(1) AS snapshot_diff_count
-- FROM `charging_pile_order`.`t_card_charging_order` o
-- JOIN `charging_pile_account`.`t_charging_card` c ON c.`id` = o.`card_id`
-- WHERE o.`del_flag` = 0
--   AND o.`company_id` IS NOT NULL
--   AND c.`company_id` <> o.`company_id`;