From 83188a6c8ea2f123519378da736382a9136a4331 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期一, 28 九月 2026 14:47:09 +0800
Subject: [PATCH] 新增加功能和修改已有功能
---
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/mapper/VinManagementMapperStructureTest.java | 44 ++
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/query/VinChargingOrderQuery.java | 6
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/feignClient/GroupCompanyClient.java | 6
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VinChargingOrderMapperStructureTest.java | 4
sql/20260928_app_user_whitelist_name_status.sql | 8
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistQueryDTO.java | 12
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImpl.java | 561 +++++++++++++++++++-------
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TAppUserWhitelist.java | 24 +
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinChargingOrder.java | 8
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TVinChargingOrderCompanyFleetTest.java | 192 +++++++++
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistSaveDTO.java | 17
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/factory/GroupCompanyFallbackFactory.java | 5
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistImportDTO.java | 8
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java | 76 +++
ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TVinChargingOrderMapper.xml | 2
sql/20260928_vin_charging_order_company_fleet.sql | 128 ++++++
ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TAppUserWhitelistServiceImplTest.java | 16
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserWhitelistController.java | 2
sql/20260928_vin_charging_order_company_fleet_backfill.sql | 56 ++
doc/耗时统计.md | 1
ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TVinManagementMapper.xml | 2
ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TAppUserWhitelistMapper.xml | 10
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/VinChargingOrderExport.java | 4
sql/20260928_app_user_whitelist_company_fleet_quota.sql | 19
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TGroupCompanyController.java | 20
ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java | 8
26 files changed, 1,075 insertions(+), 164 deletions(-)
diff --git "a/doc/\350\200\227\346\227\266\347\273\237\350\256\241.md" "b/doc/\350\200\227\346\227\266\347\273\237\350\256\241.md"
index cbe4fa2..d76787e 100644
--- "a/doc/\350\200\227\346\227\266\347\273\237\350\256\241.md"
+++ "b/doc/\350\200\227\346\227\266\347\273\237\350\256\241.md"
@@ -14,3 +14,4 @@
| 2026-09-15 17:39:31 | 2026-09-15 17:52:02 | 751 | 请继续执行未完成的任务。(续作:SD 已结束未扣款补偿) | 是:账户侧新增批量扣款流水查询接口,订单侧扫描状态5且应扣款的刷卡订单,逐单复核无扣款流水后条件回退为停止中交由既有补偿结算;本轮补齐“已有流水不回退”“账户查询失败不回退”“无候选不远程调用”“未抢到锁不回退”“定时任务异常隔离”5 个回归用例 | 完成 SD 已结束(状态5)订单无充电扣款流水的定时兜底回退,并补齐测试与全量回归;默认 surefire 版本无法执行 JUnit 5,改用本地仓库 surefire 2.22.2 执行 | `ruoyi-api/ruoyi-api-account/.../feignClient/ChargingCardClient.java`;`ruoyi-api/ruoyi-api-account/.../factory/ChargingCardFallbackFactory.java`;`ruoyi-service/ruoyi-account/.../controller/TChargingCardController.java`;`.../mapper/TChargingCardMapper.java`;`.../service/TChargingCardService.java`;`.../service/impl/TChargingCardServiceImpl.java`;`.../resources/mapper/account/TChargingCardMapper.xml`;`.../test/.../TChargingCardControllerTest.java`;`.../test/.../TChargingCardServiceImplTest.java`;`ruoyi-service/ruoyi-order/.../service/TCardChargingOrderService.java`;`.../service/impl/TCardChargingOrderServiceImpl.java`;`.../service/impl/TVinChargingOrderServiceImpl.java`;`.../util/task/TaskUtil.java`;`.../test/.../TCardChargingOrderAmountFieldsTest.java`;`.../test/.../TaskUtilCompensationTest.java`;`doc/耗时统计.md` | `mvn -o -pl ruoyi-api/ruoyi-api-account,ruoyi-service/ruoyi-account,ruoyi-service/ruoyi-order -am test-compile -DskipTests` BUILD SUCCESS;surefire 2.22.2 定向 14/14、ruoyi-order 全量 185/185、ruoyi-account 全量 103/103 全部通过;IDE lint 0 条;`git diff --check` 通过;代码走查通过 | 开始时间取本次续作会话首个命令时间,耗时仅统计续作会话(751 秒);实现主体在当天更早会话完成、时间未记录,故整体任务耗时未合并统计;新增用例首轮因在 thenReturn 参数内调用 Mockito 桩方法触发 UnfinishedStubbing,提取局部变量后修复 |
| 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 |
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistImportDTO.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistImportDTO.java
index 4e9535f..0679dba 100644
--- a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistImportDTO.java
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistImportDTO.java
@@ -15,12 +15,4 @@
@Excel(name = "用户手机号", width = 18, prompt = "填写11位手机号,如:13800138000")
@ApiModelProperty(value = "用户手机号")
private String phone;
-
- @Excel(name = "享受权益", width = 18, prompt = "1=全部免费,2=免电费,3=免服务费", combo = {"1", "2", "3"})
- @ApiModelProperty(value = "享受权益")
- private String benefitType;
-
- @Excel(name = "白名单电站", width = 40, prompt = "填写all表示全部电站;指定电站填写平台站点编号,多个用英文逗号分隔,如:ST001,ST002")
- @ApiModelProperty(value = "白名单电站")
- private String whitelistSite;
}
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistQueryDTO.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistQueryDTO.java
index 0935bdf..9cc34dc 100644
--- a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistQueryDTO.java
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistQueryDTO.java
@@ -16,4 +16,16 @@
@ApiModelProperty(value = "用户手机号")
private String phone;
+
+ @ApiModelProperty(value = "用户姓名")
+ private String name;
+
+ @ApiModelProperty(value = "所属公司ID")
+ private Integer companyId;
+
+ @ApiModelProperty(value = "所属车队ID")
+ private Integer fleetId;
+
+ @ApiModelProperty(value = "状态:1=启用,2=停用")
+ private Integer status;
}
diff --git a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistSaveDTO.java b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistSaveDTO.java
index f5f8677..467c705 100644
--- a/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistSaveDTO.java
+++ b/ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserWhitelistSaveDTO.java
@@ -19,7 +19,13 @@
@ApiModelProperty(value = "用户手机号")
private String phone;
- @ApiModelProperty(value = "享受权益:1=全部免费,2=免电费,3=免服务费")
+ @ApiModelProperty(value = "用户姓名(选填)")
+ private String name;
+
+ @ApiModelProperty(value = "状态:1=启用,2=停用(不传默认启用)")
+ private Integer status;
+
+ @ApiModelProperty(value = "享受权益:1=全部免费,2=免电费,3=免服务费(不传默认全部免费)")
private Integer benefitType;
@ApiModelProperty(value = "白名单电站范围:1=全部电站,2=指定站点,3=指定合作商")
@@ -28,6 +34,15 @@
@ApiModelProperty(value = "合作商ID")
private Integer partnerId;
+ @ApiModelProperty(value = "所属公司ID(关联t_group_company.id)")
+ private Integer companyId;
+
+ @ApiModelProperty(value = "所属车队ID(关联t_fleet.id),必须归属所选公司")
+ private Integer fleetId;
+
+ @ApiModelProperty(value = "初始分配额度(元),仅分配模式公司可填写,保存后从公司可用余额扣减")
+ private java.math.BigDecimal initQuota;
+
@ApiModelProperty(value = "站点ID列表")
private List<Integer> siteIds;
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 42a2d5a..9168598 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
@@ -35,6 +35,10 @@
@TableField("phone")
private String phone;
+ @ApiModelProperty(value = "用户姓名(选填)")
+ @TableField("name")
+ private String name;
+
@ApiModelProperty(value = "享受权益:1=全部免费,2=免电费,3=免服务费")
@TableField("benefit_type")
private Integer benefitType;
@@ -47,6 +51,26 @@
@TableField("partner_id")
private Integer partnerId;
+ @ApiModelProperty(value = "所属公司ID(关联t_group_company.id,跨库仅存ID)")
+ @TableField("company_id")
+ private Integer companyId;
+
+ @ApiModelProperty(value = "所属车队ID(关联t_fleet.id,跨库仅存ID)")
+ @TableField("fleet_id")
+ private Integer fleetId;
+
+ @ApiModelProperty(value = "初始分配额度(元):仅分配模式公司可设置,保存时从公司可用余额扣减")
+ @TableField("init_quota")
+ private java.math.BigDecimal initQuota;
+
+ @ApiModelProperty(value = "所属公司名称(列表远程回填,不落库)")
+ @TableField(exist = false)
+ private String companyName;
+
+ @ApiModelProperty(value = "所属车队名称(列表远程回填,不落库)")
+ @TableField(exist = false)
+ private String fleetName;
+
@ApiModelProperty(value = "状态:1=启用,2=停用")
@TableField("status")
private Integer status;
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinChargingOrder.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinChargingOrder.java
index cbc210f..bca2012 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinChargingOrder.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/model/TVinChargingOrder.java
@@ -17,4 +17,12 @@
@TableField("vin_id")
private Long vinId;
+ @ApiModelProperty("所属公司ID(建单时VIN记录快照,关联t_group_company.id)")
+ @TableField("company_id")
+ private Integer companyId;
+
+ @ApiModelProperty("所属车队ID(建单时VIN记录快照,关联t_fleet.id)")
+ @TableField("fleet_id")
+ private Integer fleetId;
+
}
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/query/VinChargingOrderQuery.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/query/VinChargingOrderQuery.java
index 09d21c1..8b20c2f 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/query/VinChargingOrderQuery.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/query/VinChargingOrderQuery.java
@@ -40,6 +40,12 @@
@ApiModelProperty("充电枪ID")
private Integer gunId;
+ @ApiModelProperty("所属公司ID")
+ private Integer companyId;
+
+ @ApiModelProperty("所属车队ID")
+ private Integer fleetId;
+
@ApiModelProperty("充电开始时间范围的开始时间")
private String startTime1;
diff --git a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
index 0836079..5deb00e 100644
--- a/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
+++ b/ruoyi-api/ruoyi-api-order/src/main/java/com/ruoyi/order/api/vo/ChargingOrderVO.java
@@ -60,6 +60,14 @@
private String vin;
@ApiModelProperty(value = "VIN管理记录ID(VIN码充电订单)")
private Long vinId;
+ @ApiModelProperty(value = "所属公司ID(VIN码充电订单建单快照)")
+ private Integer companyId;
+ @ApiModelProperty(value = "所属公司名称(按公司ID远程回填)")
+ private String companyName;
+ @ApiModelProperty(value = "所属车队ID(VIN码充电订单建单快照)")
+ private Integer fleetId;
+ @ApiModelProperty(value = "所属车队名称(按车队ID远程回填)")
+ private String fleetName;
@ApiModelProperty(value = "时段数")
private Integer count;
@ApiModelProperty(value = "充电结束原因")
diff --git a/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/factory/GroupCompanyFallbackFactory.java b/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/factory/GroupCompanyFallbackFactory.java
index cf93c56..4a8f163 100644
--- a/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/factory/GroupCompanyFallbackFactory.java
+++ b/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/factory/GroupCompanyFallbackFactory.java
@@ -33,6 +33,11 @@
}
@Override
+ public R<List<TGroupCompany>> listCompaniesByIds(List<Integer> ids) {
+ return R.fail("查询公司列表失败:" + throwable.getMessage());
+ }
+
+ @Override
public R<BigDecimal> deductBalance(Integer companyId, BigDecimal amount) {
return R.fail("扣减公司可用余额失败:" + throwable.getMessage());
}
diff --git a/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/feignClient/GroupCompanyClient.java b/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/feignClient/GroupCompanyClient.java
index 794a393..360cbbb 100644
--- a/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/feignClient/GroupCompanyClient.java
+++ b/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/feignClient/GroupCompanyClient.java
@@ -29,6 +29,12 @@
R<TGroupCompany> getCompanyById(@PathVariable("id") Integer id);
/**
+ * 按ID批量查询公司(VIN 码充电订单列表回填公司名称使用)
+ */
+ @PostMapping("/group-company/remote/list-by-ids")
+ R<List<TGroupCompany>> listCompaniesByIds(@RequestBody List<Integer> ids);
+
+ /**
* 扣减公司可用余额(条件更新,余额不足返回失败,返回扣减后余额)
*/
@PostMapping("/group-company/remote/deduct-balance")
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 1bcdfc2..05585ce 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
@@ -85,7 +85,7 @@
public void importTemplate(HttpServletResponse response) {
ExcelUtil<AppUserWhitelistImportDTO> util = new ExcelUtil<>(AppUserWhitelistImportDTO.class);
util.importTemplateExcel(response, "白名单用户导入模板",
- "填写说明:用户手机号填写11位手机号;享受权益填写 1=全部免费、2=免电费、3=免服务费;白名单电站填写 all 表示全部电站,指定电站填写平台站点编号,多个用英文逗号分隔,如 ST001,ST002。");
+ "填写说明:用户手机号填写11位手机号,如:13800138000。导入后默认享受全部免费权益、适用于全部电站。");
}
@ApiOperation("导入白名单用户")
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 db281a9..5d32587 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
@@ -29,6 +29,9 @@
import com.ruoyi.common.core.web.domain.BasePojo;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.common.security.utils.SecurityUtils;
+import com.ruoyi.other.api.domain.TFleet;
+import com.ruoyi.other.api.domain.TGroupCompany;
+import com.ruoyi.other.api.feignClient.GroupCompanyClient;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.feignClient.SysUserClient;
import com.ruoyi.system.api.model.LoginUser;
@@ -36,6 +39,11 @@
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
@@ -45,6 +53,8 @@
import javax.annotation.Resource;
import java.io.InputStream;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -54,6 +64,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -63,10 +74,10 @@
@Service
public class TAppUserWhitelistServiceImpl extends ServiceImpl<TAppUserWhitelistMapper, TAppUserWhitelist> implements TAppUserWhitelistService {
+ private static final Logger log = LoggerFactory.getLogger(TAppUserWhitelistServiceImpl.class);
+
private static final Pattern PHONE_PATTERN = Pattern.compile("^1\\d{10}$");
private static final String IMPORT_HEADER_PHONE = "用户手机号";
- private static final String IMPORT_HEADER_BENEFIT_TYPE = "享受权益";
- private static final String IMPORT_HEADER_WHITELIST_SITE = "白名单电站";
private static final int BENEFIT_ALL_FREE = 1;
private static final int BENEFIT_ELECTRIC_FREE = 2;
private static final int BENEFIT_SERVICE_FREE = 3;
@@ -74,6 +85,20 @@
private static final int SCOPE_SELECTED_SITE = 2;
private static final int SCOPE_PARTNER = 3;
private static final int STATUS_ENABLED = 1;
+ private static final int STATUS_DISABLED = 2;
+ /** 姓名最大长度(对应 varchar(64)) */
+ private static final int MAX_NAME_LENGTH = 64;
+
+ /** 公司账户模式:2=分配模式(可设置初始分配额度并扣减公司可用余额) */
+ private static final int ACCOUNT_MODE_ALLOCATION = 2;
+ /** 初始分配额度上限(对应 decimal(12,2)) */
+ private static final BigDecimal MAX_INIT_QUOTA = new BigDecimal("99999999.99");
+ /** 初始分配额度并发锁前缀:新增按手机号、编辑按ID串行,防止并发下重复扣减或退还公司余额 */
+ private static final String QUOTA_LOCK_PREFIX = "APP_USER_WHITELIST_QUOTA:";
+ /** 获取锁最长等待时间(秒) */
+ private static final long LOCK_WAIT_SECONDS = 5L;
+ /** 锁自动释放时间(秒),兜住异常未解锁的场景 */
+ private static final long LOCK_LEASE_SECONDS = 30L;
@Resource
private TAppUserWhitelistSiteService whitelistSiteService;
@@ -89,6 +114,10 @@
private SysUserClient sysUserClient;
@Resource
private PlatformTransactionManager transactionManager;
+ @Autowired
+ private GroupCompanyClient groupCompanyClient;
+ @Autowired
+ private RedissonClient redissonClient;
@Override
public Page<TAppUserWhitelist> pageList(AppUserWhitelistQueryDTO queryDTO) {
@@ -99,6 +128,10 @@
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())
.eq(TAppUserWhitelist::getDelFlag, false);
if (currentPartnerId != null) {
if (StringUtils.isNotBlank(username)) {
@@ -171,7 +204,6 @@
throw new ServiceException("导入数据不能为空");
}
- Map<String, Site> siteCodeMap = getSiteCodeMap();
Map<String, AppUserWhitelistSaveDTO> validRows = new LinkedHashMap<>();
Map<String, Integer> validRowNumbers = new HashMap<>();
List<AppUserWhitelistImportErrorVO> errors = new ArrayList<>();
@@ -179,12 +211,12 @@
for (int i = 0; i < rows.size(); i++) {
AppUserWhitelistImportDTO row = rows.get(i);
int rowNum = titleNum + i + 2;
- List<String> rowErrors = validateImportRow(row, siteCodeMap);
+ List<String> rowErrors = validateImportRow(row);
if (!rowErrors.isEmpty()) {
errors.add(new AppUserWhitelistImportErrorVO(rowNum, normalize(row.getPhone()), String.join(";", rowErrors)));
continue;
}
- AppUserWhitelistSaveDTO dto = buildSaveDTO(row, siteCodeMap);
+ AppUserWhitelistSaveDTO dto = buildSaveDTO(row);
if (!importPhones.add(dto.getPhone())) {
errors.add(new AppUserWhitelistImportErrorVO(rowNum, dto.getPhone(), "导入文件中手机号重复"));
continue;
@@ -314,9 +346,7 @@
return false;
}
ExcelUtil<AppUserWhitelistImportDTO> util = new ExcelUtil<>(AppUserWhitelistImportDTO.class);
- return IMPORT_HEADER_PHONE.equals(normalize(String.valueOf(util.getCellValue(row, 0))))
- && IMPORT_HEADER_BENEFIT_TYPE.equals(normalize(String.valueOf(util.getCellValue(row, 1))))
- && IMPORT_HEADER_WHITELIST_SITE.equals(normalize(String.valueOf(util.getCellValue(row, 2))));
+ return IMPORT_HEADER_PHONE.equals(normalize(String.valueOf(util.getCellValue(row, 0))));
}
private void validateSaveDTO(AppUserWhitelistSaveDTO saveDTO) {
@@ -327,17 +357,36 @@
if (!isValidPhone(saveDTO.getPhone())) {
throw new ServiceException("请输入正确的用户手机号");
}
- if (!isValidBenefit(saveDTO.getBenefitType())) {
+ saveDTO.setName(normalize(saveDTO.getName()));
+ if (saveDTO.getName() != null && saveDTO.getName().isEmpty()) {
+ saveDTO.setName(null);
+ }
+ if (StringUtils.isNotBlank(saveDTO.getName()) && saveDTO.getName().length() > MAX_NAME_LENGTH) {
+ throw new ServiceException("姓名长度不能超过" + MAX_NAME_LENGTH + "个字符");
+ }
+ // 状态不传默认启用,仅允许1=启用、2=停用
+ Integer status = saveDTO.getStatus() == null ? STATUS_ENABLED : saveDTO.getStatus();
+ if (!Objects.equals(status, STATUS_ENABLED) && !Objects.equals(status, STATUS_DISABLED)) {
+ throw new ServiceException("请选择正确的状态");
+ }
+ saveDTO.setStatus(status);
+ // 享受权益不传默认全部免费,传了仍按原规则校验
+ Integer benefitType = saveDTO.getBenefitType() == null ? BENEFIT_ALL_FREE : saveDTO.getBenefitType();
+ if (!isValidBenefit(benefitType)) {
throw new ServiceException("请选择正确的享受权益");
}
- if (!isValidScope(saveDTO.getScopeType())) {
+ saveDTO.setBenefitType(benefitType);
+ // 白名单电站不传默认全部电站,传了仍按原规则校验
+ Integer scopeType = saveDTO.getScopeType() == null ? SCOPE_ALL_SITE : saveDTO.getScopeType();
+ if (!isValidScope(scopeType)) {
throw new ServiceException("请选择正确的白名单电站范围");
}
- if (Objects.equals(saveDTO.getScopeType(), SCOPE_SELECTED_SITE)
+ saveDTO.setScopeType(scopeType);
+ if (Objects.equals(scopeType, SCOPE_SELECTED_SITE)
&& (saveDTO.getSiteIds() == null || saveDTO.getSiteIds().isEmpty())) {
throw new ServiceException("请选择白名单电站");
}
- if (Objects.equals(saveDTO.getScopeType(), SCOPE_PARTNER) && saveDTO.getPartnerId() == null) {
+ if (Objects.equals(scopeType, SCOPE_PARTNER) && saveDTO.getPartnerId() == null) {
throw new ServiceException("请选择加盟商");
}
validatePartnerDataScope(saveDTO);
@@ -354,81 +403,155 @@
if (getByPhone(saveDTO.getPhone()) != null) {
throw new ServiceException("该手机号已存在白名单配置");
}
- TAppUserWhitelist whitelist = new TAppUserWhitelist();
- TAppUser appUser = appUserService.lambdaQuery()
- .eq(TAppUser::getPhone, saveDTO.getPhone())
- .last("LIMIT 1")
- .one();
- whitelist.setCreateBy(getUsername());
- whitelist.setAppUserId(appUser == null ? null : appUser.getId());
- whitelist.setPhone(saveDTO.getPhone());
- whitelist.setBenefitType(saveDTO.getBenefitType());
- whitelist.setScopeType(saveDTO.getScopeType());
- whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO));
- whitelist.setStatus(STATUS_ENABLED);
- whitelist.setUpdateBy(getUsername());
- whitelist.setRemark(saveDTO.getRemark());
- whitelist.setDelFlag(false);
- if (whitelist.getId() == null) {
- this.save(whitelist);
- }
-
- whitelistSiteService.deleteByWhitelistId(whitelist.getId());
- if (Objects.equals(saveDTO.getScopeType(), SCOPE_SELECTED_SITE)) {
- Long whitelistId = whitelist.getId();
- List<TAppUserWhitelistSite> sites = saveDTO.getSiteIds().stream()
- .filter(Objects::nonNull)
- .distinct()
- .map(siteId -> {
- TAppUserWhitelistSite site = new TAppUserWhitelistSite();
- site.setWhitelistId(whitelistId);
- site.setSiteId(siteId);
- site.setDelFlag(false);
- return site;
- }).collect(Collectors.toList());
- if (!sites.isEmpty()) {
- whitelistSiteService.saveBatch(sites);
+ // 同手机号并发新增串行化:手机号重复校验与额度扣减在同一临界区内
+ RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + "PHONE:" + saveDTO.getPhone());
+ boolean locked = tryQuotaLock(lock, "该手机号正在处理中,请稍后重试");
+ List<Runnable> compensations = new ArrayList<>();
+ try {
+ if (getByPhone(saveDTO.getPhone()) != null) {
+ throw new ServiceException("该手机号已存在白名单配置");
}
+ BigDecimal quota = normalizeQuota(saveDTO.getInitQuota());
+ Integer companyId = resolveRelationCompanyId(saveDTO);
+ if (quota.signum() > 0) {
+ requireAllocationCompany(companyId);
+ deductBalance(companyId, quota);
+ compensations.add(() -> refundQuietly(companyId, quota));
+ }
+ TAppUserWhitelist whitelist = new TAppUserWhitelist();
+ TAppUser appUser = appUserService.lambdaQuery()
+ .eq(TAppUser::getPhone, saveDTO.getPhone())
+ .last("LIMIT 1")
+ .one();
+ whitelist.setCreateBy(getUsername());
+ whitelist.setAppUserId(appUser == null ? null : appUser.getId());
+ whitelist.setPhone(saveDTO.getPhone());
+ whitelist.setName(saveDTO.getName());
+ whitelist.setBenefitType(saveDTO.getBenefitType());
+ whitelist.setScopeType(saveDTO.getScopeType());
+ whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO));
+ whitelist.setCompanyId(companyId);
+ whitelist.setFleetId(saveDTO.getFleetId());
+ whitelist.setInitQuota(quota.signum() > 0 ? quota : null);
+ whitelist.setStatus(saveDTO.getStatus());
+ whitelist.setUpdateBy(getUsername());
+ whitelist.setRemark(saveDTO.getRemark());
+ whitelist.setDelFlag(false);
+ if (whitelist.getId() == null) {
+ this.save(whitelist);
+ }
+
+ whitelistSiteService.deleteByWhitelistId(whitelist.getId());
+ if (Objects.equals(saveDTO.getScopeType(), SCOPE_SELECTED_SITE)) {
+ Long whitelistId = whitelist.getId();
+ List<TAppUserWhitelistSite> sites = saveDTO.getSiteIds().stream()
+ .filter(Objects::nonNull)
+ .distinct()
+ .map(siteId -> {
+ TAppUserWhitelistSite site = new TAppUserWhitelistSite();
+ site.setWhitelistId(whitelistId);
+ site.setSiteId(siteId);
+ site.setDelFlag(false);
+ return site;
+ }).collect(Collectors.toList());
+ if (!sites.isEmpty()) {
+ whitelistSiteService.saveBatch(sites);
+ }
+ }
+ } catch (RuntimeException ex) {
+ compensate(compensations);
+ throw ex;
+ } finally {
+ unlockQuota(lock, locked);
}
}
private void updateWhitelist(AppUserWhitelistSaveDTO saveDTO) {
- TAppUserWhitelist whitelist = this.getById(saveDTO.getId());
- if (whitelist == null) {
+ TAppUserWhitelist existing = this.getById(saveDTO.getId());
+ if (existing == null) {
throw new ServiceException("白名单用户不存在");
}
- assertPartnerVisible(whitelist);
- TAppUser appUser = appUserService.lambdaQuery()
- .eq(TAppUser::getPhone, saveDTO.getPhone())
- .last("LIMIT 1")
- .one();
- whitelist.setAppUserId(appUser == null ? null : appUser.getId());
- whitelist.setPhone(saveDTO.getPhone());
- whitelist.setBenefitType(saveDTO.getBenefitType());
- whitelist.setScopeType(saveDTO.getScopeType());
- whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO));
- whitelist.setStatus(STATUS_ENABLED);
- whitelist.setUpdateBy(getUsername());
- whitelist.setRemark(saveDTO.getRemark());
- whitelist.setDelFlag(false);
- this.baseMapper.updateByIdIgnoreLogic(whitelist);
-
- whitelistSiteService.deleteByWhitelistId(whitelist.getId());
- if (Objects.equals(saveDTO.getScopeType(), SCOPE_SELECTED_SITE)) {
- Long whitelistId = whitelist.getId();
- List<TAppUserWhitelistSite> sites = saveDTO.getSiteIds().stream()
- .filter(Objects::nonNull)
- .distinct()
- .map(siteId -> {
- TAppUserWhitelistSite site = new TAppUserWhitelistSite();
- site.setWhitelistId(whitelistId);
- site.setSiteId(siteId);
- site.setDelFlag(false);
- return site;
- }).collect(Collectors.toList());
- if (!sites.isEmpty()) {
- whitelistSiteService.saveBatch(sites);
+ assertPartnerVisible(existing);
+ // 同白名单并发编辑串行化:额度差额计算、公司余额扣减/退还必须在临界区内,防止重复扣减或重复退还
+ RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + saveDTO.getId());
+ boolean locked = tryQuotaLock(lock, "该白名单额度正在处理中,请稍后重试");
+ List<Runnable> compensations = new ArrayList<>();
+ try {
+ TAppUserWhitelist whitelist = this.getById(saveDTO.getId());
+ if (whitelist == null) {
+ throw new ServiceException("白名单用户不存在");
}
+ assertPartnerVisible(whitelist);
+ BigDecimal newQuota = normalizeQuota(saveDTO.getInitQuota());
+ Integer newCompanyId = resolveRelationCompanyId(saveDTO);
+ if (newQuota.signum() > 0) {
+ requireAllocationCompany(newCompanyId);
+ }
+ Integer oldCompanyId = whitelist.getCompanyId();
+ BigDecimal oldQuota = normalizeQuota(whitelist.getInitQuota());
+ if (Objects.equals(oldCompanyId, newCompanyId)) {
+ // 同一公司按差额调整:调增扣差额,调减退差额
+ BigDecimal diff = newQuota.subtract(oldQuota);
+ if (diff.signum() > 0 && newCompanyId != null) {
+ deductBalance(newCompanyId, diff);
+ compensations.add(() -> refundQuietly(newCompanyId, diff));
+ } else if (diff.signum() < 0 && newCompanyId != null) {
+ BigDecimal refund = diff.negate();
+ refundBalance(newCompanyId, refund);
+ compensations.add(() -> deductQuietly(newCompanyId, refund));
+ }
+ } else {
+ // 跨公司:旧公司全额退还,新公司全额扣减
+ if (oldCompanyId != null && oldQuota.signum() > 0) {
+ refundBalance(oldCompanyId, oldQuota);
+ compensations.add(() -> deductQuietly(oldCompanyId, oldQuota));
+ }
+ if (newCompanyId != null && newQuota.signum() > 0) {
+ deductBalance(newCompanyId, newQuota);
+ compensations.add(() -> refundQuietly(newCompanyId, newQuota));
+ }
+ }
+ TAppUser appUser = appUserService.lambdaQuery()
+ .eq(TAppUser::getPhone, saveDTO.getPhone())
+ .last("LIMIT 1")
+ .one();
+ whitelist.setAppUserId(appUser == null ? null : appUser.getId());
+ whitelist.setPhone(saveDTO.getPhone());
+ whitelist.setName(saveDTO.getName());
+ whitelist.setBenefitType(saveDTO.getBenefitType());
+ whitelist.setScopeType(saveDTO.getScopeType());
+ whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO));
+ whitelist.setCompanyId(newCompanyId);
+ whitelist.setFleetId(saveDTO.getFleetId());
+ whitelist.setInitQuota(newQuota.signum() > 0 ? newQuota : null);
+ whitelist.setStatus(saveDTO.getStatus());
+ whitelist.setUpdateBy(getUsername());
+ whitelist.setRemark(saveDTO.getRemark());
+ whitelist.setDelFlag(false);
+ this.baseMapper.updateByIdIgnoreLogic(whitelist);
+
+ whitelistSiteService.deleteByWhitelistId(whitelist.getId());
+ if (Objects.equals(saveDTO.getScopeType(), SCOPE_SELECTED_SITE)) {
+ Long whitelistId = whitelist.getId();
+ List<TAppUserWhitelistSite> sites = saveDTO.getSiteIds().stream()
+ .filter(Objects::nonNull)
+ .distinct()
+ .map(siteId -> {
+ TAppUserWhitelistSite site = new TAppUserWhitelistSite();
+ site.setWhitelistId(whitelistId);
+ site.setSiteId(siteId);
+ site.setDelFlag(false);
+ return site;
+ }).collect(Collectors.toList());
+ if (!sites.isEmpty()) {
+ whitelistSiteService.saveBatch(sites);
+ }
+ }
+ } catch (RuntimeException ex) {
+ compensate(compensations);
+ throw ex;
+ } finally {
+ unlockQuota(lock, locked);
}
}
@@ -442,7 +565,7 @@
return this.baseMapper.selectValidByPhone(phone);
}
- private List<String> validateImportRow(AppUserWhitelistImportDTO row, Map<String, Site> siteCodeMap) {
+ private List<String> validateImportRow(AppUserWhitelistImportDTO row) {
List<String> errors = new ArrayList<>();
if (row == null) {
errors.add("空行");
@@ -452,57 +575,17 @@
if (!isValidPhone(phone)) {
errors.add("用户手机号格式错误");
}
- Integer benefitType = parseInteger(row.getBenefitType());
- if (!isValidBenefit(benefitType)) {
- errors.add("享受权益只能填写1、2、3");
- }
- String whitelistSite = normalize(row.getWhitelistSite());
- if (StringUtils.isBlank(whitelistSite)) {
- errors.add("白名单电站不能为空");
- } else if (!"all".equalsIgnoreCase(whitelistSite)) {
- for (String code : whitelistSite.split(",")) {
- String siteCode = normalize(code);
- if (StringUtils.isBlank(siteCode)) {
- errors.add("白名单电站存在空编号");
- } else if (!siteCodeMap.containsKey(siteCode)) {
- errors.add("站点编号不存在:" + siteCode);
- }
- }
- }
return errors;
}
- private AppUserWhitelistSaveDTO buildSaveDTO(AppUserWhitelistImportDTO row, Map<String, Site> siteCodeMap) {
+ /** 导入数据统一按默认值处理:全部免费 + 全部电站 */
+ private AppUserWhitelistSaveDTO buildSaveDTO(AppUserWhitelistImportDTO row) {
AppUserWhitelistSaveDTO dto = new AppUserWhitelistSaveDTO();
dto.setPhone(normalize(row.getPhone()));
- dto.setBenefitType(parseInteger(row.getBenefitType()));
- String whitelistSite = normalize(row.getWhitelistSite());
- if ("all".equalsIgnoreCase(whitelistSite)) {
- dto.setScopeType(SCOPE_ALL_SITE);
- dto.setSiteIds(Collections.emptyList());
- } else {
- dto.setScopeType(SCOPE_SELECTED_SITE);
- List<Integer> siteIds = new ArrayList<>();
- for (String code : whitelistSite.split(",")) {
- Site site = siteCodeMap.get(normalize(code));
- if (site != null) {
- siteIds.add(site.getId());
- }
- }
- dto.setSiteIds(siteIds.stream().distinct().collect(Collectors.toList()));
- }
+ dto.setBenefitType(BENEFIT_ALL_FREE);
+ dto.setScopeType(SCOPE_ALL_SITE);
+ dto.setSiteIds(Collections.emptyList());
return dto;
- }
-
- private Map<String, Site> getSiteCodeMap() {
- List<Site> sites = getCurrentSelectableSites();
- Map<String, Site> siteCodeMap = new HashMap<>();
- for (Site site : sites) {
- if (site != null && StringUtils.isNotBlank(site.getCode())) {
- siteCodeMap.put(site.getCode(), site);
- }
- }
- return siteCodeMap;
}
private void fillDisplay(List<TAppUserWhitelist> records) {
@@ -536,6 +619,59 @@
}
record.setSiteList(sites);
record.setSiteIds(sites.stream().map(TAppUserWhitelistSite::getSiteId).collect(Collectors.toList()));
+ }
+ fillCompanyNames(records);
+ fillFleetNames(records);
+ }
+
+ /** 列表回填所属公司名称(远程失败仅记录日志,不影响列表查询) */
+ private void fillCompanyNames(List<TAppUserWhitelist> records) {
+ Map<Integer, String> nameMap = new HashMap<>();
+ 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);
+ }
+ }
+
+ /** 列表回填所属车队名称(批量查询,远程失败仅记录日志,不影响列表查询) */
+ private void fillFleetNames(List<TAppUserWhitelist> records) {
+ List<Integer> fleetIds = records.stream()
+ .map(TAppUserWhitelist::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());
+ }
+ }
+ records.forEach(record -> record.setFleetName(nameMap.get(record.getFleetId())));
+ } catch (Exception ex) {
+ log.warn("回填车队名称失败,fleetIds={}", fleetIds, ex);
}
}
@@ -623,21 +759,155 @@
return Objects.equals(saveDTO.getScopeType(), SCOPE_PARTNER) ? saveDTO.getPartnerId() : null;
}
- private List<Site> getCurrentSelectableSites() {
- Integer currentPartnerId = getCurrentPartnerId();
- if (currentPartnerId == null) {
- R<List<Site>> siteResult = siteClient.getSiteAll();
- return siteResult == null || siteResult.getData() == null ? Collections.emptyList() : siteResult.getData();
+ /** 校验并回填所属公司与车队:车队必须归属所选公司;未选公司时按车队自动带出公司 */
+ private Integer resolveRelationCompanyId(AppUserWhitelistSaveDTO saveDTO) {
+ Integer companyId = saveDTO.getCompanyId();
+ if (companyId != null && loadCompany(companyId) == null) {
+ throw new ServiceException("所属公司不存在或已删除");
}
- Set<Integer> siteIds = getCurrentPartnerSiteIds(currentPartnerId);
- if (siteIds.isEmpty()) {
- return Collections.emptyList();
+ Integer fleetId = saveDTO.getFleetId();
+ if (fleetId != null) {
+ TFleet fleet = loadFleet(fleetId);
+ if (fleet == null) {
+ throw new ServiceException("所属车队不存在或已删除");
+ }
+ if (companyId == null) {
+ companyId = fleet.getCompanyId();
+ } else if (!companyId.equals(fleet.getCompanyId())) {
+ throw new ServiceException("所属车队不属于所选公司");
+ }
}
- R<List<Site>> siteResult = siteClient.getSiteByIds(new ArrayList<>(siteIds));
- List<Site> sites = siteResult == null || siteResult.getData() == null ? Collections.emptyList() : siteResult.getData();
- return sites.stream()
- .filter(site -> site != null && Objects.equals(site.getPartnerId(), currentPartnerId))
- .collect(Collectors.toList());
+ return companyId;
+ }
+
+ /** 规范化初始分配额度:null 视为 0,不允许负数、超过2位小数或超出上限 */
+ private BigDecimal normalizeQuota(BigDecimal quota) {
+ if (quota == null) {
+ return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ }
+ if (quota.signum() < 0) {
+ throw new ServiceException("初始分配额度不能小于0");
+ }
+ if (quota.stripTrailingZeros().scale() > 2) {
+ throw new ServiceException("初始分配额度最多保留2位小数");
+ }
+ BigDecimal normalized = quota.setScale(2, RoundingMode.HALF_UP);
+ if (normalized.compareTo(MAX_INIT_QUOTA) > 0) {
+ throw new ServiceException("初始分配额度不能超过" + MAX_INIT_QUOTA.toPlainString());
+ }
+ return normalized;
+ }
+
+ /** 校验公司为启用且分配模式(可设置初始分配额度) */
+ private void requireAllocationCompany(Integer companyId) {
+ if (companyId == null) {
+ throw new ServiceException("请选择所属公司后再填写初始分配额度");
+ }
+ TGroupCompany company = loadCompany(companyId);
+ if (company == null) {
+ throw new ServiceException("所属公司不存在或已删除");
+ }
+ if (!Integer.valueOf(ACCOUNT_MODE_ALLOCATION).equals(company.getAccountMode())) {
+ throw new ServiceException("仅分配模式的公司可填写初始分配额度");
+ }
+ if (!Integer.valueOf(1).equals(company.getStatus())) {
+ throw new ServiceException("所属公司已停用,无法填写初始分配额度");
+ }
+ }
+
+ private TGroupCompany loadCompany(Integer companyId) {
+ R<TGroupCompany> response = groupCompanyClient.getCompanyById(companyId);
+ if (response == null || !R.isSuccess(response)) {
+ throw new ServiceException("公司信息查询失败:" + (response == null ? "服务无响应" : response.getMsg()));
+ }
+ return response.getData();
+ }
+
+ private TFleet loadFleet(Integer fleetId) {
+ R<TFleet> response = groupCompanyClient.getFleetById(fleetId);
+ if (response == null || !R.isSuccess(response)) {
+ throw new ServiceException("车队信息查询失败:" + (response == null ? "服务无响应" : response.getMsg()));
+ }
+ return response.getData();
+ }
+
+ /** 扣减公司可用余额(余额不足或服务异常时抛出业务异常) */
+ private void deductBalance(Integer companyId, BigDecimal amount) {
+ R<BigDecimal> response = groupCompanyClient.deductBalance(companyId, amount);
+ if (response == null || !R.isSuccess(response)) {
+ throw new ServiceException("公司可用余额扣减失败:" + (response == null ? "服务无响应" : response.getMsg()));
+ }
+ }
+
+ /** 退还公司可用余额(失败时抛出业务异常) */
+ private void refundBalance(Integer companyId, BigDecimal amount) {
+ R<BigDecimal> response = groupCompanyClient.refundBalance(companyId, amount);
+ if (response == null || !R.isSuccess(response)) {
+ throw new ServiceException("公司可用余额退还失败:" + (response == null ? "服务无响应" : response.getMsg()));
+ }
+ }
+
+ /** 静默退还(补偿场景使用,失败仅记录日志) */
+ private void refundQuietly(Integer companyId, BigDecimal amount) {
+ try {
+ R<BigDecimal> response = groupCompanyClient.refundBalance(companyId, amount);
+ if (response == null || !R.isSuccess(response)) {
+ log.error("公司可用余额退还失败,需人工核对,companyId={}, amount={}, msg={}",
+ companyId, amount, response == null ? "服务无响应" : response.getMsg());
+ }
+ } catch (Exception ex) {
+ log.error("公司可用余额退还异常,需人工核对,companyId={}, amount={}", companyId, amount, ex);
+ }
+ }
+
+ /** 静默扣减(补偿场景使用,失败仅记录日志) */
+ private void deductQuietly(Integer companyId, BigDecimal amount) {
+ try {
+ R<BigDecimal> response = groupCompanyClient.deductBalance(companyId, amount);
+ if (response == null || !R.isSuccess(response)) {
+ log.error("公司可用余额回滚扣减失败,需人工核对,companyId={}, amount={}, msg={}",
+ companyId, amount, response == null ? "服务无响应" : response.getMsg());
+ }
+ } catch (Exception ex) {
+ log.error("公司可用余额回滚扣减异常,需人工核对,companyId={}, amount={}", companyId, amount, ex);
+ }
+ }
+
+ /** 保存/更新失败时反向补偿已执行的余额操作(倒序执行) */
+ private void compensate(List<Runnable> compensations) {
+ for (int i = compensations.size() - 1; i >= 0; i--) {
+ try {
+ compensations.get(i).run();
+ } catch (Exception ex) {
+ log.error("公司可用余额补偿失败,需人工核对", ex);
+ }
+ }
+ }
+
+ /** 抢占并发锁,抢占失败抛出业务异常 */
+ private boolean tryQuotaLock(RLock lock, String busyMessage) {
+ try {
+ if (!lock.tryLock(LOCK_WAIT_SECONDS, LOCK_LEASE_SECONDS, TimeUnit.SECONDS)) {
+ throw new ServiceException(busyMessage);
+ }
+ return true;
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new ServiceException(busyMessage);
+ }
+ }
+
+ private void unlockQuota(RLock lock, boolean locked) {
+ if (!locked) {
+ return;
+ }
+ try {
+ if (lock.isHeldByCurrentThread()) {
+ lock.unlock();
+ }
+ } catch (Exception ex) {
+ log.warn("释放白名单额度并发锁失败", ex);
+ }
}
private Set<Integer> getCurrentPartnerSiteIds(Integer partnerId) {
@@ -706,21 +976,6 @@
return Objects.equals(scopeType, SCOPE_ALL_SITE)
|| Objects.equals(scopeType, SCOPE_SELECTED_SITE)
|| Objects.equals(scopeType, SCOPE_PARTNER);
- }
-
- private Integer parseInteger(String value) {
- try {
- String normalizedValue = normalize(value);
- if (StringUtils.isBlank(normalizedValue)) {
- return null;
- }
- if (normalizedValue.matches("^\\d+\\.0+$")) {
- normalizedValue = normalizedValue.substring(0, normalizedValue.indexOf("."));
- }
- return Integer.valueOf(normalizedValue);
- } catch (Exception e) {
- return null;
- }
}
private String normalize(String value) {
diff --git a/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TAppUserWhitelistMapper.xml b/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TAppUserWhitelistMapper.xml
index df52329..8acea72 100644
--- a/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TAppUserWhitelistMapper.xml
+++ b/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TAppUserWhitelistMapper.xml
@@ -6,9 +6,13 @@
<id column="id" property="id" />
<result column="app_user_id" property="appUserId" />
<result column="phone" property="phone" />
+ <result column="name" property="name" />
<result column="benefit_type" property="benefitType" />
<result column="scope_type" property="scopeType" />
<result column="partner_id" property="partnerId" />
+ <result column="company_id" property="companyId" />
+ <result column="fleet_id" property="fleetId" />
+ <result column="init_quota" property="initQuota" />
<result column="status" property="status" />
<result column="create_by" property="createBy" />
<result column="create_time" property="createTime" />
@@ -18,7 +22,7 @@
</resultMap>
<sql id="Base_Column_List">
- id, app_user_id, phone, benefit_type, scope_type, partner_id, status, create_by, create_time, update_by, update_time, remark, del_flag
+ id, app_user_id, phone, name, benefit_type, scope_type, partner_id, company_id, fleet_id, init_quota, status, create_by, create_time, update_by, update_time, remark, del_flag
</sql>
<select id="selectValidByPhone" resultMap="BaseResultMap">
@@ -34,9 +38,13 @@
<set>
app_user_id = #{appUserId},
phone = #{phone},
+ name = #{name},
benefit_type = #{benefitType},
scope_type = #{scopeType},
partner_id = #{partnerId},
+ company_id = #{companyId},
+ fleet_id = #{fleetId},
+ init_quota = #{initQuota},
status = #{status},
update_by = #{updateBy},
update_time = now(),
diff --git a/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TVinManagementMapper.xml b/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TVinManagementMapper.xml
index 2e9cf10..5856f50 100644
--- a/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TVinManagementMapper.xml
+++ b/ruoyi-service/ruoyi-account/src/main/resources/mapper/account/TVinManagementMapper.xml
@@ -19,6 +19,8 @@
<if test="query != null and query.plateNumber != null and query.plateNumber != ''">AND plate_number LIKE CONCAT('%', #{query.plateNumber}, '%')</if>
<if test="query != null and query.phone != null and query.phone != ''">AND phone LIKE CONCAT('%', #{query.phone}, '%')</if>
<if test="query != null and query.status != null">AND status = #{query.status}</if>
+ <if test="query != null and query.companyId != null">AND company_id = #{query.companyId}</if>
+ <if test="query != null and query.fleetId != null">AND fleet_id = #{query.fleetId}</if>
ORDER BY create_time DESC, id DESC
</select>
<select id="countByVin" resultType="int">SELECT COUNT(1) FROM t_vin_management WHERE vin = #{vin}<if test="excludeId != null"> AND id != #{excludeId}</if></select>
diff --git a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/mapper/VinManagementMapperStructureTest.java b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/mapper/VinManagementMapperStructureTest.java
new file mode 100644
index 0000000..4d1d241
--- /dev/null
+++ b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/mapper/VinManagementMapperStructureTest.java
@@ -0,0 +1,44 @@
+package com.ruoyi.account.mapper;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * VIN 管理 Mapper 结构断言:列表与导出必须共用同一套筛选条件(含所属公司/车队),避免导出结果与列表不一致。
+ */
+class VinManagementMapperStructureTest {
+
+ @Test
+ void pageListAndExportShareCompanyAndFleetFilters() throws IOException {
+ InputStream input = getClass().getClassLoader().getResourceAsStream("mapper/account/TVinManagementMapper.xml");
+ assertNotNull(input);
+ String mapper;
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
+ mapper = reader.lines().collect(Collectors.joining("\n")).toLowerCase();
+ }
+ assertTrue(mapper.contains("select id=\"selectpagelist\""));
+ assertTrue(mapper.contains("select id=\"selectexportlist\""));
+ assertEquals(2, occurrences(mapper, "and company_id = #{query.companyid}"));
+ assertEquals(2, occurrences(mapper, "and fleet_id = #{query.fleetid}"));
+ }
+
+ private int occurrences(String content, String keyword) {
+ int count = 0;
+ int index = content.indexOf(keyword);
+ while (index >= 0) {
+ count++;
+ index = content.indexOf(keyword, index + keyword.length());
+ }
+ return count;
+ }
+}
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 f7d525a..b5b55f6 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
@@ -11,6 +11,7 @@
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;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.BeforeEach;
@@ -18,6 +19,8 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
@@ -25,10 +28,12 @@
import java.io.ByteArrayOutputStream;
import java.util.Collections;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -44,6 +49,8 @@
@Mock private TokenService tokenService;
@Mock private SysUserClient sysUserClient;
@Mock private PlatformTransactionManager transactionManager;
+ @Mock private GroupCompanyClient groupCompanyClient;
+ @Mock private RedissonClient redissonClient;
private TAppUserWhitelistServiceImpl service;
@@ -58,6 +65,15 @@
ReflectionTestUtils.setField(service, "tokenService", tokenService);
ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient);
ReflectionTestUtils.setField(service, "transactionManager", transactionManager);
+ ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient);
+ ReflectionTestUtils.setField(service, "redissonClient", redissonClient);
+ RLock lock = mock(RLock.class);
+ when(redissonClient.getLock(any())).thenReturn(lock);
+ try {
+ when(lock.tryLock(anyLong(), anyLong(), any(TimeUnit.class))).thenReturn(true);
+ } catch (InterruptedException exception) {
+ throw new IllegalStateException(exception);
+ }
}
@Test
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/VinChargingOrderExport.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/VinChargingOrderExport.java
index 26bbafe..dbb0906 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/VinChargingOrderExport.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/export/VinChargingOrderExport.java
@@ -16,6 +16,10 @@
private String code;
@Excel(name = "VIN码", width = 24)
private String vin;
+ @Excel(name = "所属公司", width = 24)
+ private String companyName;
+ @Excel(name = "所属车队", width = 20)
+ private String fleetName;
@Excel(name = "站点名称", width = 24)
private String siteName;
@Excel(name = "充电终端名称", width = 24)
diff --git a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java
index 1216e35..9ac1315 100644
--- a/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java
+++ b/ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java
@@ -52,6 +52,9 @@
import com.ruoyi.order.service.TVinChargingOrderService;
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 ChargingPileClient chargingPileClient;
@Resource
private SendMessageClient sendMessageClient;
+ @Resource
+ private GroupCompanyClient groupCompanyClient;
@Resource
private UploadRealTimeMonitoringDataService monitoringDataService;
@Resource
@@ -538,6 +543,7 @@
item.setPartnerIncome(BigDecimal.ZERO);
}
fillVinInfoAndPhone(list);
+ fillCompanyFleetNames(list);
}
/**
@@ -581,6 +587,73 @@
}
}
}
+ }
+
+ /**
+ * 回填所属公司/车队名称:按订单快照 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("[VD回填] 批量查询公司名称失败 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("[VD回填] 批量查询车队名称失败 ids={}", fleetIds, e);
+ }
+ return result;
}
/** 手机号回填优先级:订单值 → VIN 记录手机号 → App 用户当前手机号 */
@@ -733,6 +806,9 @@
order.setExpectedVin(vinRecord.getVin());
order.setActualVin(vin);
order.setPlateNum(vinRecord.getPlateNumber());
+ // 所属公司/车队快照落库(VIN 管理记录当前归属),供订单列表/汇总/导出按公司、车队直接筛选
+ order.setCompanyId(vinRecord.getCompanyId());
+ order.setFleetId(vinRecord.getFleetId());
order.setCreateTime(LocalDateTime.now());
save(order);
diff --git a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TVinChargingOrderMapper.xml b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TVinChargingOrderMapper.xml
index 46e4146..07b27c8 100644
--- a/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TVinChargingOrderMapper.xml
+++ b/ruoyi-service/ruoyi-order/src/main/resources/mapper/order/TVinChargingOrderMapper.xml
@@ -15,6 +15,8 @@
</if>
<if test="query.plateNum != null and query.plateNum != ''">AND o.plate_num LIKE CONCAT('%', #{query.plateNum}, '%')</if>
<if test="query.phone != null and query.phone != ''">AND o.phone LIKE CONCAT('%', #{query.phone}, '%')</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>
<if test="query.siteId != null">AND o.site_id = #{query.siteId}</if>
<if test="query.pileId != null">AND o.charging_pile_id = #{query.pileId}</if>
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VinChargingOrderMapperStructureTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VinChargingOrderMapperStructureTest.java
index 601627c..9133b7d 100644
--- a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VinChargingOrderMapperStructureTest.java
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/mapper/VinChargingOrderMapperStructureTest.java
@@ -29,6 +29,10 @@
assertTrue(mapper.contains("query.platenum"));
assertTrue(mapper.contains("query.phone"));
assertTrue(mapper.contains("query.siteids"));
+ assertTrue(mapper.contains("query.companyid"));
+ assertTrue(mapper.contains("query.fleetid"));
+ assertTrue(mapper.contains("o.company_id = #{query.companyid}"));
+ assertTrue(mapper.contains("o.fleet_id = #{query.fleetid}"));
assertTrue(mapper.contains("countaccountingstrategies"));
assertTrue(mapper.contains("group by charging_order_id"));
assertTrue(mapper.contains("pageappletorders"));
diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TVinChargingOrderCompanyFleetTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TVinChargingOrderCompanyFleetTest.java
new file mode 100644
index 0000000..faac3b7
--- /dev/null
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TVinChargingOrderCompanyFleetTest.java
@@ -0,0 +1,192 @@
+package com.ruoyi.order.service.impl;
+
+import com.ruoyi.account.api.feignClient.VinManagementClient;
+import com.ruoyi.account.api.model.TVinManagement;
+import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
+import com.ruoyi.chargingPile.api.model.TChargingGun;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
+import com.ruoyi.order.api.model.TVinChargingOrder;
+import com.ruoyi.order.api.vo.ChargingOrderVO;
+import com.ruoyi.order.api.vo.DeviceStartChargingVO;
+import com.ruoyi.order.mapper.TVinChargingOrderMapper;
+import com.ruoyi.other.api.domain.TFleet;
+import com.ruoyi.other.api.domain.TGroupCompany;
+import com.ruoyi.other.api.feignClient.GroupCompanyClient;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.ArrayList;
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * VIN码充电订单「所属公司/所属车队」单测:
+ * 1) 建单时把 VIN 管理记录的 companyId/fleetId 作为快照写入订单;
+ * 2) 列表/导出按订单快照 companyId/fleetId 批量远程取名,失败或缺失时保持为空且不抛异常。
+ */
+@ExtendWith(MockitoExtension.class)
+class TVinChargingOrderCompanyFleetTest {
+
+ private static final String VIN = "LFV3A23C8K3123456";
+
+ @InjectMocks
+ private TVinChargingOrderServiceImpl service;
+
+ @Mock
+ private TVinChargingOrderMapper mapper;
+ @Mock
+ private ChargingGunClient chargingGunClient;
+ @Mock
+ private VinManagementClient vinManagementClient;
+ @Mock
+ private GroupCompanyClient groupCompanyClient;
+ @Mock
+ private org.redisson.api.RedissonClient redissonClient;
+ @Mock
+ private org.redisson.api.RLock lock;
+
+ private void fillCompanyFleetNames(List<ChargingOrderVO> list) {
+ ReflectionTestUtils.invokeMethod(service, "fillCompanyFleetNames", list);
+ }
+
+ @Test
+ void writesCompanyAndFleetSnapshotWhenBuildingOrder() throws Exception {
+ ReflectionTestUtils.setField(service, "baseMapper", mapper);
+ when(redissonClient.getLock(anyString())).thenReturn(lock);
+ when(lock.tryLock(anyLong(), anyLong(), any(TimeUnit.class))).thenReturn(true);
+ lenient().when(lock.isHeldByCurrentThread()).thenReturn(true);
+ when(mapper.insert(any(TVinChargingOrder.class))).thenReturn(1);
+
+ TChargingGun gun = new TChargingGun();
+ gun.setId(6);
+ when(chargingGunClient.getChargingGunByCode(any())).thenReturn(R.ok(gun));
+
+ TVinManagement vinRecord = new TVinManagement();
+ vinRecord.setId(1L);
+ vinRecord.setVin(VIN);
+ vinRecord.setPlateNumber("川A123456");
+ vinRecord.setStatus(1);
+ vinRecord.setCompanyId(12);
+ vinRecord.setFleetId(3);
+ when(vinManagementClient.getVinByCode(VIN)).thenReturn(R.ok(vinRecord));
+
+ DeviceStartChargingDTO dto = new DeviceStartChargingDTO();
+ dto.setChargingPileCode("P001");
+ dto.setChargingGunCode("G001");
+ dto.setAccount(new StringBuilder(VIN).reverse().toString());
+
+ DeviceStartChargingVO vo = service.startVinCharging(dto);
+
+ assertEquals(Integer.valueOf(1), vo.getAuthentication());
+ ArgumentCaptor<TVinChargingOrder> captor = ArgumentCaptor.forClass(TVinChargingOrder.class);
+ verify(mapper).insert(captor.capture());
+ TVinChargingOrder saved = captor.getValue();
+ assertEquals(Integer.valueOf(12), saved.getCompanyId());
+ assertEquals(Integer.valueOf(3), saved.getFleetId());
+ assertEquals(Long.valueOf(1L), saved.getVinId());
+ assertEquals(VIN, saved.getActualVin());
+ assertEquals(VIN, saved.getExpectedVin());
+ assertEquals("川A123456", saved.getPlateNum());
+ }
+
+ @Test
+ void fillsCompanyAndFleetNamesFromOrderSnapshot() {
+ when(groupCompanyClient.listCompaniesByIds(anyList()))
+ .thenReturn(R.ok(Collections.singletonList(company(12, "甲公司"))));
+ when(groupCompanyClient.listFleetsByIds(anyList()))
+ .thenReturn(R.ok(Collections.singletonList(fleet(3, "一队"))));
+
+ ChargingOrderVO withFleet = order(12, 3);
+ ChargingOrderVO companyOnly = order(12, null);
+ ChargingOrderVO unmatched = order(99, 88);
+ List<ChargingOrderVO> list = new ArrayList<>(Arrays.asList(withFleet, companyOnly, unmatched));
+
+ fillCompanyFleetNames(list);
+
+ assertEquals("甲公司", withFleet.getCompanyName());
+ assertEquals("一队", withFleet.getFleetName());
+ assertEquals("甲公司", companyOnly.getCompanyName());
+ assertNull(companyOnly.getFleetName());
+ assertNull(unmatched.getCompanyName());
+ assertNull(unmatched.getFleetName());
+
+ ArgumentCaptor<List<Integer>> companyIds = ArgumentCaptor.forClass(List.class);
+ verify(groupCompanyClient).listCompaniesByIds(companyIds.capture());
+ assertTrue(companyIds.getValue().containsAll(Arrays.asList(12, 99)));
+ assertEquals(2, companyIds.getValue().size());
+ ArgumentCaptor<List<Integer>> fleetIds = ArgumentCaptor.forClass(List.class);
+ verify(groupCompanyClient).listFleetsByIds(fleetIds.capture());
+ assertTrue(fleetIds.getValue().containsAll(Arrays.asList(3, 88)));
+ assertEquals(2, fleetIds.getValue().size());
+ }
+
+ @Test
+ void keepsNamesBlankWhenRemoteFails() {
+ when(groupCompanyClient.listCompaniesByIds(anyList())).thenReturn(R.fail("公司服务调用失败"));
+ when(groupCompanyClient.listFleetsByIds(anyList())).thenThrow(new RuntimeException("车队服务不可用"));
+
+ ChargingOrderVO item = order(12, 3);
+ List<ChargingOrderVO> list = new ArrayList<>(Collections.singletonList(item));
+
+ fillCompanyFleetNames(list);
+
+ assertNull(item.getCompanyName());
+ assertNull(item.getFleetName());
+ }
+
+ @Test
+ void skipsRemoteCallsWhenOrdersHaveNoOwnership() {
+ ChargingOrderVO item = new ChargingOrderVO();
+ item.setCode("VD202609280001");
+ List<ChargingOrderVO> list = new ArrayList<>(Collections.singletonList(item));
+
+ fillCompanyFleetNames(list);
+
+ verify(groupCompanyClient, never()).listCompaniesByIds(anyList());
+ verify(groupCompanyClient, never()).listFleetsByIds(anyList());
+ assertNull(item.getCompanyName());
+ assertNull(item.getFleetName());
+ }
+
+ private ChargingOrderVO order(Integer companyId, Integer fleetId) {
+ ChargingOrderVO item = new ChargingOrderVO();
+ item.setCode("VD202609280002");
+ item.setCompanyId(companyId);
+ item.setFleetId(fleetId);
+ return item;
+ }
+
+ private TGroupCompany company(Integer id, String name) {
+ TGroupCompany company = new TGroupCompany();
+ company.setId(id);
+ company.setName(name);
+ return company;
+ }
+
+ private TFleet fleet(Integer id, String fleetName) {
+ TFleet fleet = new TFleet();
+ fleet.setId(id);
+ fleet.setFleetName(fleetName);
+ return fleet;
+ }
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TGroupCompanyController.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TGroupCompanyController.java
index f7eaf9e..04cad2a 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TGroupCompanyController.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TGroupCompanyController.java
@@ -22,6 +22,10 @@
import javax.annotation.Resource;
import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
/**
* <p>
@@ -222,4 +226,20 @@
return R.ok(latest == null ? null : latest.getAvailableBalance());
}
+ /**
+ * 远程接口:按ID批量查询公司(VIN 码充电订单列表回填公司名称使用,内部调用)
+ */
+ @PostMapping("/remote/list-by-ids")
+ @ApiOperation(value = "远程-按ID批量查询公司", tags = {"内部调用"})
+ public R<List<TGroupCompany>> remoteListByIds(@RequestBody List<Integer> ids) {
+ if (ids == null || ids.isEmpty()) {
+ return R.ok(new ArrayList<>());
+ }
+ List<Integer> distinct = ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
+ if (distinct.isEmpty()) {
+ return R.ok(new ArrayList<>());
+ }
+ return R.ok(groupCompanyService.listByIds(distinct));
+ }
+
}
diff --git a/sql/20260928_app_user_whitelist_company_fleet_quota.sql b/sql/20260928_app_user_whitelist_company_fleet_quota.sql
new file mode 100644
index 0000000..81575d3
--- /dev/null
+++ b/sql/20260928_app_user_whitelist_company_fleet_quota.sql
@@ -0,0 +1,19 @@
+-- 白名单管理增加:所属公司、所属车队、初始分配额度
+-- 创建日期: 2026-09-28
+-- 目标库: charging_pile_account(Mycat 8066 直接执行)
+-- 说明: company_id 关联 t_group_company.id(charging_pile_other 库);fleet_id 关联 t_fleet.id(charging_pile_other 库);跨库仅存ID,不建外键
+-- init_quota 为白名单初始分配额度(元),仅"分配模式"公司可填写并扣减公司可用余额;删除白名单不退还额度(对齐卡片管理)
+-- 注意: 非幂等,列已存在时执行会报 Duplicate column,忽略即可(以 information_schema 回查为准)
+
+ALTER TABLE `charging_pile_account`.`t_app_user_whitelist`
+ ADD COLUMN `company_id` int NULL DEFAULT NULL COMMENT '所属公司ID(关联t_group_company.id)' AFTER `partner_id`;
+ALTER TABLE `charging_pile_account`.`t_app_user_whitelist`
+ ADD COLUMN `fleet_id` int NULL DEFAULT NULL COMMENT '所属车队ID(关联t_fleet.id)' AFTER `company_id`;
+ALTER TABLE `charging_pile_account`.`t_app_user_whitelist`
+ ADD COLUMN `init_quota` decimal(12,2) NULL DEFAULT NULL COMMENT '初始分配额度(元)' AFTER `fleet_id`;
+
+-- 辅助索引(可选,代理不支持时可跳过,不影响功能)
+ALTER TABLE `charging_pile_account`.`t_app_user_whitelist`
+ ADD KEY `idx_company_id` (`company_id`);
+ALTER TABLE `charging_pile_account`.`t_app_user_whitelist`
+ ADD KEY `idx_fleet_id` (`fleet_id`);
diff --git a/sql/20260928_app_user_whitelist_name_status.sql b/sql/20260928_app_user_whitelist_name_status.sql
new file mode 100644
index 0000000..4f69126
--- /dev/null
+++ b/sql/20260928_app_user_whitelist_name_status.sql
@@ -0,0 +1,8 @@
+-- 白名单管理:新增姓名列(status 列已存在,无需重复添加)
+-- 创建日期: 2026-09-28
+-- 目标库: charging_pile_account(Mycat 8066 直接执行)
+-- 说明: name 为用户姓名,选填,仅作展示用途
+-- 注意: 非幂等,列已存在时执行会报 Duplicate column,忽略即可(以 information_schema 回查为准)
+
+ALTER TABLE `charging_pile_account`.`t_app_user_whitelist`
+ ADD COLUMN `name` varchar(64) NULL DEFAULT NULL COMMENT '用户姓名' AFTER `phone`;
diff --git a/sql/20260928_vin_charging_order_company_fleet.sql b/sql/20260928_vin_charging_order_company_fleet.sql
new file mode 100644
index 0000000..757abd8
--- /dev/null
+++ b/sql/20260928_vin_charging_order_company_fleet.sql
@@ -0,0 +1,128 @@
+-- VIN码充电订单新增「所属公司 / 所属车队」快照列
+-- 目的:VIN 码充电订单列表/汇总/导出支持按公司、车队筛选与展示(方案B:订单表冗余归属列 + SQL 直接筛选)
+-- 目标库:charging_pile_order(表 t_vin_charging_order,TBPARTITION BY MOD_HASH(vin_id) TBPARTITIONS 20)
+-- 数据来源:建单时取 t_vin_management(VIN 管理记录)的 company_id / fleet_id 快照;
+-- 历史订单回填见同目录 20260928_vin_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 再变更,可重复执行。
+
+-- ============ 小节 A:经代理执行(推荐) ============
+USE `charging_pile_order`;
+
+ALTER TABLE `t_vin_charging_order`
+ ADD COLUMN `company_id` int DEFAULT NULL COMMENT '所属公司ID(建单时VIN记录快照,关联t_group_company.id)';
+
+ALTER TABLE `t_vin_charging_order`
+ ADD COLUMN `fleet_id` int DEFAULT NULL COMMENT '所属车队ID(建单时VIN记录快照,关联t_fleet.id)';
+
+ALTER TABLE `t_vin_charging_order` ADD KEY `idx_company_id` (`company_id`);
+ALTER TABLE `t_vin_charging_order` ADD KEY `idx_fleet_id` (`fleet_id`);
+
+-- ============ 小节 B:直连后端 MySQL 实例执行(代理不可用时分表执行,幂等) ============
+USE `charging_pile_order`;
+
+DROP PROCEDURE IF EXISTS `add_vin_charging_order_company_fleet`;
+DELIMITER $$
+CREATE PROCEDURE `add_vin_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_vin_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_vin_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(建单时VIN记录快照,关联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(建单时VIN记录快照,关联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_vin_charging_order_company_fleet`();
+DROP PROCEDURE `add_vin_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_vin_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_vin_charging_order(_[0-9]+)?$'
+ AND c.column_name IN ('company_id', 'fleet_id');
diff --git a/sql/20260928_vin_charging_order_company_fleet_backfill.sql b/sql/20260928_vin_charging_order_company_fleet_backfill.sql
new file mode 100644
index 0000000..74a2558
--- /dev/null
+++ b/sql/20260928_vin_charging_order_company_fleet_backfill.sql
@@ -0,0 +1,56 @@
+-- VIN码充电订单「所属公司 / 所属车队」历史数据回填脚本
+-- 前置:已执行 20260928_vin_charging_order_company_fleet.sql(两列已存在)
+-- 数据来源:charging_pile_account.t_vin_management(含 company_id / fleet_id / del_flag)
+-- 执行方式:MyCat 不支持跨逻辑库 UPDATE JOIN,故分三步人工执行:
+-- 步骤1 在 charging_pile_account 生成 UPDATE 语句(单库只读查询);
+-- 步骤2 把步骤1 结果集逐行复制到 charging_pile_order 执行(vin_id 为分片键,可路由单分片);
+-- 步骤3 回查校验。
+-- 语义与幂等:
+-- * 仅回填 company_id IS NULL 的历史订单行,不覆盖上线后新建订单的建单时快照;
+-- * 同一 VIN 重复执行结果一致(写入 VIN 记录当前归属);
+-- * 车辆未分配公司且未分配车队的 VIN 不生成语句,其历史订单两列保持 NULL。
+
+-- ============ 步骤 0:前置检查(charging_pile_account,只读) ============
+USE `charging_pile_account`;
+
+SELECT COUNT(1) AS assignable_vehicle_count
+FROM `t_vin_management`
+WHERE `del_flag` = 0 AND (`company_id` IS NOT NULL OR `fleet_id` IS NOT NULL);
+
+SELECT COUNT(1) AS unassigned_vehicle_count
+FROM `t_vin_management`
+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_vin_charging_order` SET `company_id` = ', IFNULL(v.`company_id`, 'NULL'),
+ ', `fleet_id` = ', IFNULL(v.`fleet_id`, 'NULL'),
+ ' WHERE `vin_id` = ', v.`id`, ' AND `company_id` IS NULL;') AS backfill_sql
+FROM `t_vin_management` v
+WHERE v.`del_flag` = 0
+ AND (v.`company_id` IS NOT NULL OR v.`fleet_id` IS NOT NULL)
+ORDER BY v.`id`;
+
+-- ============ 步骤 2:在 charging_pile_order 执行步骤1 生成的语句 ============
+-- 示例(vin_id = 1 的车辆归属公司 12、车队 3):
+-- USE `charging_pile_order`;
+-- UPDATE `t_vin_charging_order` SET `company_id` = 12, `fleet_id` = 3 WHERE `vin_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_vin_charging_order`;
+
+-- 以下跨逻辑库比对请【直连后端 MySQL 实例】执行(代理不支持跨库 JOIN):
+-- 归属公司与 VIN 记录不一致的订单(回填后应为 0;上线后新建订单因车辆改归属而不为 0 属预期)
+-- SELECT COUNT(1) AS snapshot_diff_count
+-- FROM `charging_pile_order`.`t_vin_charging_order` o
+-- JOIN `charging_pile_account`.`t_vin_management` v ON v.`id` = o.`vin_id`
+-- WHERE o.`del_flag` = 0
+-- AND o.`company_id` IS NOT NULL
+-- AND v.`company_id` <> o.`company_id`;
--
Gitblit v1.7.1