| | |
| | | | 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;开始时间取本轮首个产出文件创建时间,上一轮方案分析耗时未计 | |
| | | | 2026-09-28 15:44:00(估算,含授权前方案分析) | 2026-09-28 16:04:00(估算) | 约 1200(估算,含回归测试、单测修复、前端构建与推送) | 推送代码,编写完整的提交备注 | 是:先只读核查两仓库分支/远端/改动清单,输出推送方案与完整提交备注(含 DDL 部署前置提示与「不强制推送」约束),获「开始执行」授权后按仓库分别提交并普通推送 | 提交并推送刷卡订单公司/车队筛选、白名单姓名状态与公司车队额度、VIN 订单与 VIN 管理公司/车队筛选、车队查看弹窗改版四批改动;服务端提交 `aa8119b5`,管理后台提交 `686b5246`;回归中发现并修复白名单导入单测过期断言 | 服务端 10 个文件(ruoyi-api-order 两个模型、ruoyi-account 导入实现与单测、ruoyi-order 服务/导出/Mapper、sql 两个分表脚本、doc/耗时统计.md);管理后台 15 个文件(FleetSearchSelect 三件套、刷卡订单页、VIN 订单页、VIN 管理页、白名单四文件与 SiteSelectModal 删除、车队页);归档日志 chargingPile_management/logs/build-20260928-git-push.log | 后端 `mvn -pl ruoyi-api/ruoyi-api-order -am install`、order/account `test-compile` 均 BUILD SUCCESS;surefire 2.22.2 跑 ruoyi-order 189/189、ruoyi-account 133/133 全部通过;前端 `npx jest` 14 套件/68 用例、`npm run tsc`、`npm run build`(dist 271 文件)全部通过;推送后远端引用校验 83188a6c..aa8119b5(2.3)、482abf19..686b5246(dev_2.3) | 首轮 ruoyi-account 有 1 条失败:TAppUserWhitelistServiceImplTest 断言与「导入统一按默认值、仅校验手机号」新语义不符且 siteClient 打桩已无用,同步断言(默认全部免费+全部电站+启用)并补非法手机号错误行覆盖后全绿;刷卡订单 DDL 尚未在任何库执行,部署前须先执行 sql/20260928_card_charging_order_company_fleet.sql 及回填脚本,否则筛选会报 Column 'company_id' not found;本统计行在推送后追加,未随本次提交推送 | |
| | | | 2026-09-28 17:33:00(估算,含授权前方案分析与两轮确认问答) | 2026-09-28 17:39:39 | 约 399(估算,授权后执行 188 秒 + 方案分析约 211 秒) | 推送代码,编写完整的修改描述 | 是:先只读核查两仓库分支/远端/改动清单与编译产物,发现工作区含两个功能块改动(充电用户信息 + 优惠设置/集团联系人),经两轮确认问答确定「全部推送、每仓库一个 commit」后获「开始执行」授权 | 提交并推送公司详情充电用户信息聚合表格、公司优惠设置/账户模式改造、集团联系人字段三批改动;服务端提交 `29e9ce47`,管理后台提交 `a84f8954` | 服务端 38 个文件(VIN/刷卡/白名单三套 list-by-company 内部接口与 Feign/FallbackFactory、TGroupCompany 聚合与 CompanyChargingUserVO、TCompanyPreferential 全套与 CompanyPreferentialSupport、TGroup 联系人字段、sql/20260928_company_preferential.sql、sql/20260928_group_contact.sql、三个测试类、doc/耗时统计.md、sql/20260926_统一执行清单.md);管理后台 9 个文件(company/index.jsx、service.js、RechargeModal.jsx、AccountModeModal.jsx、PreferentialModal.jsx、companyPayload/preferential 及各自单测) | 后端 `mvn -pl ruoyi-api/ruoyi-api-account,ruoyi-api/ruoyi-api-other,ruoyi-service/ruoyi-account,ruoyi-service/ruoyi-other -am compile` BUILD SUCCESS(17/17 模块);前端删 dist 后 `npm run build` 成功(Webpack Compiled successfully,dist 生成);推送后远端引用核验 aa8119b5..29e9ce47(2.3)、686b5246..a84f8954(dev_2.3);IDE lint 0 错误 | company_preferential 与 group_contact 两个 DDL 尚未执行(测试库待执行,见 sql/20260926_统一执行清单.md 第十节);本统计行在推送后追加,未随本次提交推送 | |
| | | | 2026-09-28 18:07:00(估算,授权后执行阶段;含授权前方案分析与两轮确认约 19:xx 起) | 2026-09-28 18:40:44 | 约 2024(估算,授权后执行约 34 分钟;含授权前方案分析与两轮确认问答总计约 55 分钟) | 分析代码,将VIN码、卡片、白名单全部都舍弃init_quota字段,统一使用balance字段表示余额,额度分配和初始额度也是修改balance字段 | 是:先只读盘点 init_quota 全部 40 处引用(模型/DTO/Mapper/Service/前端页面/历史 SQL),输出三表统一方案(VIN/白名单 CHANGE 列名保留数据、卡片 DROP 列丢弃、前端仅改字段不改文案),经两轮确认问答(卡片历史数据直接丢弃、文案保留原表述、VIN 与白名单一同修改)后获「开始执行」授权 | 三模块舍弃 init_quota 统一 balance:VIN/白名单模型 DTO 改名 balance,卡片模型/DTO 删除 initQuota 复用既有 balance;额度分配与初始额度写库改为 balance 列;卡片编辑仅传正数余额才更新 balance(未传保持原余额);新建统一 DDL 脚本并追加执行清单第十一节 | 服务端 20 个文件(3 模型+3 DTO、TVinManagementMapper.java、三个 Mapper XML、三个 ServiceImpl、TGroupCompanyServiceImpl、三个测试类、sql/20260928_unify_quota_to_balance.sql 新建、sql/20260926_统一执行清单.md、doc/耗时统计.md);管理后台 6 个文件(vin/index.jsx、vin/AllocateModal.jsx、card/index.jsx、person/whitelist/index.jsx、whitelist/AddAndEdit.jsx、fleet/index.jsx) | 后端 `mvn -pl ruoyi-api-account,ruoyi-account,ruoyi-other -am compile` BUILD SUCCESS;surefire 2.22.2 跑 TChargingCardServiceImplTest 50/50、TVinManagementServiceImplTest 48/48、TAppUserWhitelistServiceImplTest 1/1、TFleetServiceImplTest 6/6、TGroupCompanyServiceImplTest 11/11 全部通过;前端删 dist 后 build 成功、`npx tsc --noEmit` 退出码 0;IDE lint 0 错误 | 首轮单测失败 1 条:updateCardRequiresExistingCardAndDoesNotChangeBalance 因编辑时 dto.balance=999.99 触发「请选择所属公司后再填写初始分配额度」,按新语义修正测试(编辑未传 balance 余额保持)并加固主代码(balance 仅正数才写列,避免共享模式清空余额);统一 DDL(sql/20260928_unify_quota_to_balance.sql)尚未在任何库执行,发版前必须先执行;前端工作区另有 company/index.jsx、global.less 公司树选中样式改动(非本任务,未纳入) | |
| | |
| | | @ApiModelProperty(value = "所属车队ID(关联t_fleet.id),必须归属所选公司") |
| | | private Integer fleetId; |
| | | |
| | | @ApiModelProperty(value = "初始分配额度(元),仅分配模式公司可填写,保存后从公司可用余额扣减") |
| | | private java.math.BigDecimal initQuota; |
| | | @ApiModelProperty(value = "余额(元),分配模式公司设置后从公司可用余额扣减") |
| | | private java.math.BigDecimal balance; |
| | | |
| | | @ApiModelProperty(value = "站点ID列表") |
| | | private List<Integer> siteIds; |
| | |
| | | |
| | | @ApiModelProperty(value = "所属车队ID(关联t_fleet.id),必须归属所选公司") |
| | | private Integer fleetId; |
| | | |
| | | @ApiModelProperty(value = "初始分配额度(元),仅分配模式公司可填写,保存后从公司可用余额扣减") |
| | | private BigDecimal initQuota; |
| | | } |
| | |
| | | private Integer companyId; |
| | | /** 所属车队ID(关联t_fleet.id) */ |
| | | private Integer fleetId; |
| | | /** 初始分配额度(元),仅分配模式公司可设置 */ |
| | | private BigDecimal initQuota; |
| | | /** 余额(元),分配模式公司设置后从公司可用余额扣减 */ |
| | | private BigDecimal balance; |
| | | private String remark; |
| | | private Integer status; |
| | | } |
| | |
| | | @TableField("fleet_id") |
| | | private Integer fleetId; |
| | | |
| | | @ApiModelProperty(value = "初始分配额度(元):仅分配模式公司可设置,保存时从公司可用余额扣减") |
| | | @TableField("init_quota") |
| | | private java.math.BigDecimal initQuota; |
| | | @ApiModelProperty(value = "余额(元):分配模式公司设置后从公司可用余额扣减") |
| | | @TableField("balance") |
| | | private java.math.BigDecimal balance; |
| | | |
| | | @ApiModelProperty(value = "所属公司名称(列表远程回填,不落库)") |
| | | @TableField(exist = false) |
| | |
| | | @ApiModelProperty(value = "所属车队ID") |
| | | private Integer fleetId; |
| | | |
| | | /** 初始分配额度(元):仅分配模式公司可设置,保存时从公司可用余额扣减 */ |
| | | @TableField("init_quota") |
| | | @ApiModelProperty(value = "初始分配额度(元)") |
| | | private BigDecimal initQuota; |
| | | |
| | | /** 所属公司名称(列表远程回填,不落库) */ |
| | | @TableField(exist = false) |
| | | @ApiModelProperty(value = "所属公司名称") |
| | |
| | | @Excel(name = "所属公司", width = 24) private String companyName; |
| | | @TableField("company_id") private Integer companyId; |
| | | @TableField("fleet_id") private Integer fleetId; |
| | | @TableField("init_quota") private BigDecimal initQuota; |
| | | @TableField("balance") private BigDecimal balance; |
| | | @TableField(exist = false) private String fleetName; |
| | | @Excel(name = "备注", width = 30) private String remark; |
| | | @Excel(name = "启用状态", readConverterExp = "1=启用,2=禁用") private Integer status; |
| | |
| | | IPage<TVinManagement> selectPageList(Page<TVinManagement> page, @Param("query") VinManagementQueryDTO query); |
| | | List<VinManagementExcelDTO> selectExportList(@Param("query") VinManagementQueryDTO query); |
| | | int countByVin(@Param("vin") String vin, @Param("excludeId") Long excludeId); |
| | | /** 初始分配额度原子累加(额度分配专用,null 视为 0) */ |
| | | int addInitQuota(@Param("id") Long id, @Param("amount") BigDecimal amount, @Param("updateBy") Long updateBy); |
| | | /** 余额原子累加(额度分配专用,null 视为 0) */ |
| | | int addBalance(@Param("id") Long id, @Param("amount") BigDecimal amount, @Param("updateBy") Long updateBy); |
| | | List<TVinManagement> selectAppletVehicleList(@Param("appUserId") Long appUserId); |
| | | TVinManagement selectEnabledVehicle(@Param("keyword") String keyword); |
| | | int bindAppletVehicle(@Param("id") Long id, @Param("appUserId") Long appUserId); |
| | |
| | | if (getByPhone(saveDTO.getPhone()) != null) { |
| | | throw new ServiceException("该手机号已存在白名单配置"); |
| | | } |
| | | BigDecimal quota = normalizeQuota(saveDTO.getInitQuota()); |
| | | BigDecimal quota = normalizeQuota(saveDTO.getBalance()); |
| | | Integer companyId = resolveRelationCompanyId(saveDTO); |
| | | if (quota.signum() > 0) { |
| | | requireAllocationCompany(companyId); |
| | |
| | | whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO)); |
| | | whitelist.setCompanyId(companyId); |
| | | whitelist.setFleetId(saveDTO.getFleetId()); |
| | | whitelist.setInitQuota(quota.signum() > 0 ? quota : null); |
| | | whitelist.setBalance(quota.signum() > 0 ? quota : null); |
| | | whitelist.setStatus(saveDTO.getStatus()); |
| | | whitelist.setUpdateBy(getUsername()); |
| | | whitelist.setRemark(saveDTO.getRemark()); |
| | |
| | | throw new ServiceException("白名单用户不存在"); |
| | | } |
| | | assertPartnerVisible(whitelist); |
| | | BigDecimal newQuota = normalizeQuota(saveDTO.getInitQuota()); |
| | | BigDecimal newQuota = normalizeQuota(saveDTO.getBalance()); |
| | | Integer newCompanyId = resolveRelationCompanyId(saveDTO); |
| | | if (newQuota.signum() > 0) { |
| | | requireAllocationCompany(newCompanyId); |
| | | } |
| | | Integer oldCompanyId = whitelist.getCompanyId(); |
| | | BigDecimal oldQuota = normalizeQuota(whitelist.getInitQuota()); |
| | | BigDecimal oldQuota = normalizeQuota(whitelist.getBalance()); |
| | | if (Objects.equals(oldCompanyId, newCompanyId)) { |
| | | // 同一公司按差额调整:调增扣差额,调减退差额 |
| | | BigDecimal diff = newQuota.subtract(oldQuota); |
| | |
| | | whitelist.setPartnerId(resolveWhitelistPartnerId(saveDTO)); |
| | | whitelist.setCompanyId(newCompanyId); |
| | | whitelist.setFleetId(saveDTO.getFleetId()); |
| | | whitelist.setInitQuota(newQuota.signum() > 0 ? newQuota : null); |
| | | whitelist.setBalance(newQuota.signum() > 0 ? newQuota : null); |
| | | whitelist.setStatus(saveDTO.getStatus()); |
| | | whitelist.setUpdateBy(getUsername()); |
| | | whitelist.setRemark(saveDTO.getRemark()); |
| | |
| | | public boolean saveCard(ChargingCardSaveDTO dto) { |
| | | validateCard(dto, false); |
| | | String cardNo = dto.getCardNo().trim(); |
| | | BigDecimal quota = normalizeQuota(dto.getInitQuota()); |
| | | BigDecimal quota = normalizeQuota(dto.getBalance()); |
| | | // 同卡号并发新增串行化,卡号重复校验与额度扣减在同一临界区内 |
| | | RLock lock = redissonClient.getLock(QUOTA_LOCK_PREFIX + "CARD_NO:" + cardNo); |
| | | boolean locked = tryQuotaLock(lock, "该卡号正在处理中,请稍后重试"); |
| | |
| | | try { |
| | | TChargingCard card = new TChargingCard(); |
| | | card.setCardNo(cardNo); |
| | | card.setBalance(dto.getBalance() == null ? BigDecimal.ZERO : dto.getBalance()); |
| | | card.setBalance(quota.signum() > 0 ? quota : BigDecimal.ZERO); |
| | | card.setStatus(dto.getStatus() == null ? ENABLED : dto.getStatus()); |
| | | card.setPassword(encodePasswordIfPresent(dto.getPassword())); |
| | | card.setChargingFlag(0); |
| | |
| | | deductBalance(card.getCompanyId(), quota); |
| | | compensations.add(() -> refundQuietly(card.getCompanyId(), quota)); |
| | | } |
| | | card.setInitQuota(quota.signum() > 0 ? quota : null); |
| | | if (!save(card)) { |
| | | throw new ServiceException("新增卡片失败"); |
| | | } |
| | |
| | | card.setUpdateTime(LocalDateTime.now()); |
| | | card.setUpdateBy(SecurityUtils.getUserId()); |
| | | applyRelation(card, dto); |
| | | BigDecimal newQuota = normalizeQuota(dto.getInitQuota()); |
| | | BigDecimal newQuota = normalizeQuota(dto.getBalance()); |
| | | if (newQuota.signum() > 0) { |
| | | requireAllocationCompany(card.getCompanyId()); |
| | | } |
| | | Integer oldCompanyId = latest.getCompanyId(); |
| | | BigDecimal oldQuota = normalizeQuota(latest.getInitQuota()); |
| | | BigDecimal oldQuota = normalizeQuota(latest.getBalance()); |
| | | if (Objects.equals(oldCompanyId, card.getCompanyId())) { |
| | | // 同一公司按差额调整:调增扣差额,调减退差额 |
| | | BigDecimal diff = newQuota.subtract(oldQuota); |
| | |
| | | compensations.add(() -> refundQuietly(card.getCompanyId(), newQuota)); |
| | | } |
| | | } |
| | | card.setInitQuota(newQuota.signum() > 0 ? newQuota : null); |
| | | card.setBalance(newQuota.signum() > 0 ? newQuota : null); |
| | | String encodedPassword = encodePasswordIfPresent(dto.getPassword()); |
| | | if (!updateCardRecord(card, encodedPassword, latest)) { |
| | | throw new ServiceException("修改卡片失败"); |
| | |
| | | if (!updateById(update)) { |
| | | return false; |
| | | } |
| | | BigDecimal newBalance = card.getBalance(); |
| | | boolean balanceChanged = newBalance != null && newBalance.signum() > 0 |
| | | && !quotaEquals(existing.getBalance(), newBalance); |
| | | boolean relationChanged = !Objects.equals(existing.getCompanyId(), card.getCompanyId()) |
| | | || !Objects.equals(existing.getFleetId(), card.getFleetId()) |
| | | || !quotaEquals(existing.getInitQuota(), card.getInitQuota()); |
| | | || balanceChanged; |
| | | if (!relationChanged) { |
| | | return true; |
| | | } |
| | | UpdateWrapper<TChargingCard> relationUpdate = new UpdateWrapper<>(); |
| | | relationUpdate.eq("id", card.getId()) |
| | | .set("company_id", card.getCompanyId()) |
| | | .set("fleet_id", card.getFleetId()) |
| | | .set("init_quota", card.getInitQuota()); |
| | | .set("fleet_id", card.getFleetId()); |
| | | if (balanceChanged) { |
| | | relationUpdate.set("balance", newBalance); |
| | | } |
| | | return update(null, relationUpdate); |
| | | } |
| | | |
| | |
| | | validate(dto, false); |
| | | TVinManagement item = from(dto); |
| | | applyRelation(item, dto, null); |
| | | BigDecimal quota = normalizeQuota(dto.getInitQuota()); |
| | | BigDecimal quota = normalizeQuota(dto.getBalance()); |
| | | List<Runnable> compensations = new ArrayList<>(); |
| | | try { |
| | | if (quota.signum() > 0) { |
| | |
| | | deductBalance(item.getCompanyId(), quota); |
| | | compensations.add(() -> refundQuietly(item.getCompanyId(), quota)); |
| | | } |
| | | item.setInitQuota(quota.signum() > 0 ? quota : null); |
| | | item.setBalance(quota.signum() > 0 ? quota : null); |
| | | item.setDelFlag(0); |
| | | item.setCreateTime(LocalDateTime.now()); |
| | | item.setCreateBy(SecurityUtils.getUserId()); |
| | |
| | | item.setUpdateTime(LocalDateTime.now()); |
| | | item.setUpdateBy(SecurityUtils.getUserId()); |
| | | applyRelation(item, dto, old); |
| | | BigDecimal newQuota = normalizeQuota(dto.getInitQuota()); |
| | | BigDecimal oldQuota = normalizeQuota(old.getInitQuota()); |
| | | BigDecimal newQuota = normalizeQuota(dto.getBalance()); |
| | | BigDecimal oldQuota = normalizeQuota(old.getBalance()); |
| | | if (newQuota.signum() > 0) { |
| | | requireAllocationCompany(item.getCompanyId()); |
| | | } |
| | | Integer oldCompanyId = old.getCompanyId(); |
| | | Integer newCompanyId = item.getCompanyId(); |
| | | item.setInitQuota(newQuota.signum() > 0 ? newQuota : null); |
| | | item.setBalance(newQuota.signum() > 0 ? newQuota : null); |
| | | List<Runnable> compensations = new ArrayList<>(); |
| | | try { |
| | | if (Objects.equals(oldCompanyId, newCompanyId)) { |
| | |
| | | if (!Integer.valueOf(ENABLED).equals(vehicle.getStatus())) { |
| | | throw new ServiceException("VIN码【" + vehicle.getVin() + "】已禁用,禁止分配额度"); |
| | | } |
| | | BigDecimal quota = normalizeQuota(vehicle.getInitQuota()); |
| | | BigDecimal quota = normalizeQuota(vehicle.getBalance()); |
| | | if (quota.add(amount).compareTo(MAX_INIT_QUOTA) > 0) { |
| | | throw new ServiceException("VIN码【" + vehicle.getVin() + "】分配后初始分配额度超过上限" |
| | | + MAX_INIT_QUOTA.toPlainString()); |
| | |
| | | Long operator = SecurityUtils.getUserId(); |
| | | for (TVinManagement vehicle : vehicles) { |
| | | BigDecimal amount = amounts.get(vehicle.getId()); |
| | | if (baseMapper.addInitQuota(vehicle.getId(), amount, operator) != 1) { |
| | | if (baseMapper.addBalance(vehicle.getId(), amount, operator) != 1) { |
| | | throw new ServiceException("VIN码【" + vehicle.getVin() + "】额度分配失败,请重试"); |
| | | } |
| | | } |
| | |
| | | .set(TVinManagement::getCompanyName, item.getCompanyName()) |
| | | .set(TVinManagement::getCompanyId, item.getCompanyId()) |
| | | .set(TVinManagement::getFleetId, item.getFleetId()) |
| | | .set(TVinManagement::getInitQuota, item.getInitQuota()) |
| | | .set(TVinManagement::getBalance, item.getBalance()) |
| | | .set(TVinManagement::getRemark, item.getRemark()) |
| | | .set(TVinManagement::getStatus, item.getStatus()) |
| | | .set(TVinManagement::getUpdateTime, item.getUpdateTime()) |
| | |
| | | <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="balance" property="balance" /> |
| | | <result column="status" property="status" /> |
| | | <result column="create_by" property="createBy" /> |
| | | <result column="create_time" property="createTime" /> |
| | |
| | | </resultMap> |
| | | |
| | | <sql id="Base_Column_List"> |
| | | 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 |
| | | id, app_user_id, phone, name, benefit_type, scope_type, partner_id, company_id, fleet_id, balance, status, create_by, create_time, update_by, update_time, remark, del_flag |
| | | </sql> |
| | | |
| | | <select id="selectValidByPhone" resultMap="BaseResultMap"> |
| | |
| | | partner_id = #{partnerId}, |
| | | company_id = #{companyId}, |
| | | fleet_id = #{fleetId}, |
| | | init_quota = #{initQuota}, |
| | | balance = #{balance}, |
| | | status = #{status}, |
| | | update_by = #{updateBy}, |
| | | update_time = now(), |
| | |
| | | balance, |
| | | company_id, |
| | | fleet_id, |
| | | init_quota, |
| | | status, |
| | | del_flag, |
| | | create_time, |
| | |
| | | <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
| | | <mapper namespace="com.ruoyi.account.mapper.TVinManagementMapper"> |
| | | <select id="selectPageList" resultType="com.ruoyi.account.api.model.TVinManagement"> |
| | | SELECT id, vin, plate_number, phone, company_name, company_id, fleet_id, init_quota, remark, status, del_flag, create_time, create_by, update_time, update_by |
| | | SELECT id, vin, plate_number, phone, company_name, company_id, fleet_id, balance, remark, status, del_flag, create_time, create_by, update_time, update_by |
| | | FROM t_vin_management WHERE del_flag = 0 |
| | | <if test="query != null and query.vin != null and query.vin != ''">AND vin LIKE CONCAT('%', #{query.vin}, '%')</if> |
| | | <if test="query != null and query.plateNumber != null and query.plateNumber != ''">AND plate_number LIKE CONCAT('%', #{query.plateNumber}, '%')</if> |
| | |
| | | WHERE app_user_id = #{appUserId} AND status = 1 AND del_flag = 0 |
| | | ORDER BY id ASC |
| | | </select> |
| | | <update id="addInitQuota"> |
| | | <update id="addBalance"> |
| | | UPDATE t_vin_management |
| | | SET init_quota = IFNULL(init_quota, 0) + #{amount}, update_time = NOW(), update_by = #{updateBy} |
| | | SET balance = IFNULL(balance, 0) + #{amount}, update_time = NOW(), update_by = #{updateBy} |
| | | WHERE id = #{id} AND del_flag = 0 |
| | | </update> |
| | | </mapper> |
| | |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setId(7L); |
| | | dto.setCardNo(" CARD-7 "); |
| | | dto.setBalance(new BigDecimal("999.99")); |
| | | when(mapper.countByCardNo("CARD-7", 7L)).thenReturn(0); |
| | | TChargingCard existing = new TChargingCard(); |
| | | existing.setId(7L); |
| | |
| | | dto.setCardNo(" CARD-Q1 "); |
| | | dto.setCompanyId(9); |
| | | dto.setFleetId(3); |
| | | dto.setInitQuota(new BigDecimal("100")); |
| | | dto.setBalance(new BigDecimal("100")); |
| | | |
| | | assertTrue(service.saveCard(dto)); |
| | | |
| | |
| | | verify(mapper).insert(card.capture()); |
| | | assertEquals(Integer.valueOf(9), card.getValue().getCompanyId()); |
| | | assertEquals(Integer.valueOf(3), card.getValue().getFleetId()); |
| | | assertEquals(new BigDecimal("100.00"), card.getValue().getInitQuota()); |
| | | assertEquals(new BigDecimal("100.00"), card.getValue().getBalance()); |
| | | verify(groupCompanyClient).deductBalance(9, new BigDecimal("100.00")); |
| | | } |
| | | |
| | |
| | | ChargingCardSaveDTO dto = new ChargingCardSaveDTO(); |
| | | dto.setCardNo("CARD-Q2"); |
| | | dto.setCompanyId(9); |
| | | dto.setInitQuota(new BigDecimal("10.00")); |
| | | dto.setBalance(new BigDecimal("10.00")); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, () -> service.saveCard(dto)); |
| | | |
| | |
| | | ArgumentCaptor<TChargingCard> card = ArgumentCaptor.forClass(TChargingCard.class); |
| | | verify(mapper).insert(card.capture()); |
| | | assertEquals(null, card.getValue().getCompanyId()); |
| | | assertEquals(null, card.getValue().getInitQuota()); |
| | | assertEquals(0, card.getValue().getBalance().compareTo(BigDecimal.ZERO)); |
| | | verify(groupCompanyClient, never()).deductBalance(any(), any()); |
| | | } |
| | | |
| | |
| | | TChargingCard card = new TChargingCard(); |
| | | card.setId(id); |
| | | card.setCompanyId(companyId); |
| | | card.setInitQuota(quota); |
| | | card.setBalance(quota); |
| | | return card; |
| | | } |
| | | |
| | |
| | | dto.setId(id); |
| | | dto.setCardNo(cardNo); |
| | | dto.setCompanyId(companyId); |
| | | dto.setInitQuota(quota); |
| | | dto.setBalance(quota); |
| | | return dto; |
| | | } |
| | | |
| | |
| | | doReturn(Arrays.asList(allocateVehicle(1L, 9, "VIN-1", "10.00"), allocateVehicle(2L, 9, "VIN-2", "20.00"))) |
| | | .when(service).listByIds(anyCollection()); |
| | | when(groupCompanyClient.deductBalance(9, new BigDecimal("30.00"))).thenReturn(R.ok(new BigDecimal("970.00"))); |
| | | when(mapper.addInitQuota(1L, new BigDecimal("10.00"), CURRENT_USER)).thenReturn(1); |
| | | when(mapper.addInitQuota(2L, new BigDecimal("20.00"), CURRENT_USER)).thenReturn(1); |
| | | when(mapper.addBalance(1L, new BigDecimal("10.00"), CURRENT_USER)).thenReturn(1); |
| | | when(mapper.addBalance(2L, new BigDecimal("20.00"), CURRENT_USER)).thenReturn(1); |
| | | |
| | | VinManagementAllocateVO vo = service.allocateQuota(allocateDto(9, |
| | | allocateItem(1L, "10.00"), allocateItem(2L, "20.00"))); |
| | | |
| | | assertEquals(new BigDecimal("970.00"), vo.getAvailableBalance()); |
| | | assertEquals("测试公司", vo.getCompanyName()); |
| | | verify(mapper).addInitQuota(1L, new BigDecimal("10.00"), CURRENT_USER); |
| | | verify(mapper).addInitQuota(2L, new BigDecimal("20.00"), CURRENT_USER); |
| | | verify(mapper).addBalance(1L, new BigDecimal("10.00"), CURRENT_USER); |
| | | verify(mapper).addBalance(2L, new BigDecimal("20.00"), CURRENT_USER); |
| | | verify(redissonClient).getLock("VIN_QUOTA:COMPANY:9"); |
| | | } |
| | | |
| | |
| | | doReturn(Collections.singletonList(allocateVehicle(1L, 9, "VIN-1", "10.00"))) |
| | | .when(service).listByIds(anyCollection()); |
| | | when(groupCompanyClient.deductBalance(9, new BigDecimal("10.00"))).thenReturn(R.ok(new BigDecimal("990.00"))); |
| | | when(mapper.addInitQuota(1L, new BigDecimal("10.00"), CURRENT_USER)).thenReturn(0); |
| | | when(mapper.addBalance(1L, new BigDecimal("10.00"), CURRENT_USER)).thenReturn(0); |
| | | when(groupCompanyClient.refundBalance(9, new BigDecimal("10.00"))).thenReturn(R.ok(new BigDecimal("1000.00"))); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | |
| | | return company; |
| | | } |
| | | |
| | | private TVinManagement allocateVehicle(Long id, Integer companyId, String vin, String initQuota) { |
| | | private TVinManagement allocateVehicle(Long id, Integer companyId, String vin, String balance) { |
| | | TVinManagement vehicle = new TVinManagement(); |
| | | vehicle.setId(id); |
| | | vehicle.setCompanyId(companyId); |
| | | vehicle.setVin(vin); |
| | | vehicle.setInitQuota(new BigDecimal(initQuota)); |
| | | vehicle.setBalance(new BigDecimal(balance)); |
| | | vehicle.setStatus(1); |
| | | return vehicle; |
| | | } |
| | |
| | | for (TVinManagement vin : vins) { |
| | | result.add(buildRow(USER_TYPE_VIN + "-" + vin.getId(), USER_TYPE_VIN, vin.getVin(), |
| | | companyName, fleetNameMap.get(vin.getFleetId()), accountMode, |
| | | vin.getInitQuota(), companyBalance, vin.getStatus())); |
| | | vin.getBalance(), companyBalance, vin.getStatus())); |
| | | } |
| | | for (TChargingCard card : cards) { |
| | | result.add(buildRow(USER_TYPE_CARD + "-" + card.getId(), USER_TYPE_CARD, card.getCardNo(), |
| | |
| | | for (TAppUserWhitelist whitelist : whitelists) { |
| | | result.add(buildRow(USER_TYPE_WHITELIST + "-" + whitelist.getId(), USER_TYPE_WHITELIST, whitelist.getPhone(), |
| | | companyName, fleetNameMap.get(whitelist.getFleetId()), accountMode, |
| | | whitelist.getInitQuota(), companyBalance, whitelist.getStatus())); |
| | | whitelist.getBalance(), companyBalance, whitelist.getStatus())); |
| | | } |
| | | return result; |
| | | } |
| | |
| | | item.setVin("12345678900000000"); |
| | | item.setPlateNumber("川A123456"); |
| | | item.setFleetId(4); |
| | | item.setInitQuota(new BigDecimal("30.00")); |
| | | item.setBalance(new BigDecimal("30.00")); |
| | | item.setStatus(1); |
| | | return item; |
| | | } |
| | |
| | | - 代码侧配套(无需改动):`TGroupCompanyServiceImpl` 新增/编辑/删除公司与 `sys_user`(roleType=5、role_key=company)的同步逻辑、前端公司页「登录账号」必填项此前已实现,本次仅补齐数据库结构。 |
| | | - 待办提醒:现有 1 行公司(喜望公司)`account`/`password` 为 NULL;前端编辑公司时「登录账号」为只读禁用态,补录账号需业务侧确认放开前端或后台补数据;`sys_role` 新角色的菜单权限需在「角色管理」中配置(脚本不涉及)。 |
| | | |
| | | ## 十、追加执行:公司优惠设置建表(2026-09-28,待执行) |
| | | ## 十、追加执行:公司优惠设置建表(2026-09-28,测试库已执行) |
| | | |
| | | | 脚本 | 结果 | |
| | | |---|---| |
| | | | `20260928_company_preferential.sql`(本次新建) | **待执行**:`charging_pile_other.t_company_preferential`(公司管理-优惠设置) | |
| | | | `20260928_company_preferential.sql`(本次新建) | **测试库已执行**:`charging_pile_other.t_company_preferential`(公司管理-优惠设置);2026-09-28 经外网入口 221.182.45.100:8066(与内网 192.168.110.111:8066 同一套 Mycat)`source` 执行,EXIT=0 | |
| | | |
| | | - 背景:公司管理新增「优惠设置」弹窗(1=不优惠 / 2=按比例优惠 / 3=按优惠单价收费),一个公司一条配置;充值弹窗的优惠方式配置已移除,改由该弹窗统一维护(充值记录表 `t_company_recharge_record.preferential_type` 保持库默认 1=不优惠,历史记录展示不受影响)。 |
| | | - 执行方式(Mycat 2.0,建议先复制为英文临时名再 `source`,并校验 `$LASTEXITCODE`): |
| | |
| | | ``` |
| | | - 部署后验证:公司管理 → 公司概况 → 「优惠设置」保存后重开弹窗回显、「账户模式」保存后概况账户模式同步刷新(需部署 `ruoyi-api-other` + `ruoyi-other` 新版本与前端产物)。 |
| | | - 代码侧配套(同批改动):新增实体 `TCompanyPreferential`(`service_discount_rate` / `billing_template_id` 采用 `FieldStrategy.IGNORED`,切换优惠方式时旧值可被清空)、`CompanyPreferentialDTO`、Mapper/Service/Controller(`/other/company-preferential/detail|save`)、接口 `/other/group-company/account-mode`;`TGroupCompanyServiceImpl.addCompany` 对未传账户模式的公司按共享模式(1)建档;前端添加/编辑企业弹窗已删除账户模式配置。 |
| | | - 未执行原因:本次授权范围为代码与脚本,测试库/生产库 DDL 需业务侧确认后再执行。 |
| | | - 执行证据(2026-09-28,测试库):`SHOW CREATE TABLE charging_pile_other.t_company_preferential` 返回 8 列(`id` / `company_id` / `preferential_type` / `service_discount_rate` / `billing_template_id` / `create_time` / `update_time` / `del_flag`)+ `PRIMARY KEY (id)` + `UNIQUE KEY uk_company_id USING BTREE (company_id)`,`CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT '公司优惠设置'`,中文注释无乱码;`SELECT COUNT(*)` = 0(空表);**原报错 SQL 原样重放**(`SELECT id,company_id,preferential_type,service_discount_rate,billing_template_id,update_time,create_time,del_flag FROM t_company_preferential WHERE del_flag=0 AND (company_id = 2)`)EXIT=0 正常返回空结果集;`EXPLAIN` 解析到逻辑表 `charging_pile_other`.`t_company_preferential`(Mycat targetName: prototype);`SHOW COLUMNS` 显示 `company_id` Key=UNI、`preferential_type` 默认 1、`del_flag` 默认 0。 |
| | | - 回查注意点(本次踩到):Mycat `SHOW TABLES ... LIKE 'x'` 会报 Calcite `Column 'x' not found in any table`(LIKE 字面量被当作列名),回查请改用 `SHOW CREATE TABLE` 或 `SHOW TABLES FROM <逻辑库>`;不带默认库名执行 DML 报 `ERROR 1105 (HY000) please use schema`,需在客户端命令末尾追加库名(或在脚本内 `USE`)。 |
| | | - 其他环境(生产)发版前必须先执行本脚本,否则优惠设置接口报 `Object 't_company_preferential' not found within 'charging_pile_other'`(本次测试库报错即此原因)。 |
| | | |
| | | ## 十一、追加执行:余额字段统一(2026-09-28,代码已改,DDL 待执行) |
| | | |
| | | | 脚本 | 结果 | |
| | | |---|---| |
| | | | `20260928_unify_quota_to_balance.sql`(本次新建) | **尚未执行**:VIN 码/白名单/卡片三表舍弃 `init_quota`、统一使用 `balance` 表示余额 | |
| | | |
| | | - 背景:VIN 码(`t_vin_management`)、刷卡卡(`t_charging_card`)、白名单(`t_app_user_whitelist`)的「初始分配额度」`init_quota` 与余额 `balance` 语义重复,统一舍弃 `init_quota`:VIN/白名单 `CHANGE COLUMN init_quota balance`(保留历史数据),卡片 `DROP COLUMN init_quota`(卡片已有 `balance` 列,历史分配记录不保留);额度分配与初始额度写入统一改为 `balance` 列。 |
| | | - 执行方式(Mycat 2.0,建议先复制为英文临时名再 `source`,并校验 `$LASTEXITCODE`): |
| | | ```bash |
| | | mysql -h<host> -P8066 -uroot -p --default-character-set=utf8mb4 -e "source <绝对路径>/unify_quota_to_balance.sql" |
| | | ``` |
| | | - 幂等性:脚本为纯 `ALTER TABLE`(2 条 CHANGE + 1 条 DROP),重复执行会报 `Unknown column`/`Duplicate column` 类错误,忽略即可;以 information_schema 回查为准。 |
| | | - 回查命令(执行后回填证据): |
| | | ```bash |
| | | mysql -h<host> -P8066 -uroot -p --default-character-set=utf8mb4 -e "SHOW COLUMNS FROM charging_pile_account.t_vin_management LIKE 'balance'" |
| | | mysql -h<host> -P8066 -uroot -p --default-character-set=utf8mb4 -e "SHOW COLUMNS FROM charging_pile_account.t_app_user_whitelist LIKE 'balance'" |
| | | mysql -h<host> -P8066 -uroot -p --default-character-set=utf8mb4 -e "SHOW COLUMNS FROM charging_pile_account.t_charging_card LIKE 'init_quota'" |
| | | ``` |
| | | - 部署后验证:VIN 管理/刷卡卡/白名单列表「初始分配额度」列正常显示 balance 值;新增/编辑填写额度后公司可用余额同步扣减;额度分配累加 balance。 |
| | | - 代码侧配套(同批改动):三套 domain/DTO/Mapper/Service 与聚合逻辑、前端 vin/card/whitelist/fleet 页面字段全部由 `initQuota` 改为 `balance`(界面文案保留「初始分配额度」)。 |
| | | - 其他环境(生产)发版前必须先执行本脚本,否则接口报 `Unknown column 'init_quota'`。 |
| New file |
| | |
| | | -- VIN码/白名单/卡片统一余额字段:舍弃 init_quota,统一使用 balance 表示余额 |
| | | -- 创建日期: 2026-09-28 |
| | | -- 目标库: charging_pile_account(Mycat 8066 直接执行) |
| | | -- 说明: 1) t_vin_management、t_app_user_whitelist 将 init_quota 改名 balance,历史数据保留; |
| | | -- 2) t_charging_card 已有 balance 列(卡余额),init_quota 与 balance 语义重复,直接删除列,历史分配记录不保留; |
| | | -- 3) 额度分配与初始额度写入统一改为 balance 列。 |
| | | -- 注意: 非幂等,重复执行会报错(Unknown column / Duplicate column),忽略即可(以 information_schema 回查为准) |
| | | |
| | | -- VIN码:init_quota 改名 balance(保留历史数据) |
| | | ALTER TABLE `charging_pile_account`.`t_vin_management` |
| | | CHANGE COLUMN `init_quota` `balance` decimal(12,2) NULL DEFAULT NULL COMMENT '余额(元)'; |
| | | |
| | | -- 白名单:init_quota 改名 balance(保留历史数据) |
| | | ALTER TABLE `charging_pile_account`.`t_app_user_whitelist` |
| | | CHANGE COLUMN `init_quota` `balance` decimal(12,2) NULL DEFAULT NULL COMMENT '余额(元)'; |
| | | |
| | | -- 卡片:直接删除 init_quota(与既有 balance 列语义重复,历史分配记录不保留) |
| | | ALTER TABLE `charging_pile_account`.`t_charging_card` |
| | | DROP COLUMN `init_quota`; |