| | |
| | | | 2026-09-15 09:41:00(估算) | 2026-09-15 10:33:37 | 3157(估算) | 请继续执行未完成的任务。(续作:VIN 订单首末 SOC 回填) | 是:统一结算首次处理时按监控创建时间升/降序取首末 SOC,空数据或查询异常不阻断,重复账单保持幂等 | 在 VD 正常交易记录及充电超时补偿共用结算入口中回填 `start_soc/end_soc`;增加正常映射、空监控数据及重复账单不查询 Mongo 的回归断言 | `ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TVinChargingOrderServiceImpl.java`;`ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/CardVinSettlementTimeAlignmentTest.java`;`ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/CardVinOrderCompensationTest.java`;`doc/耗时统计.md` | UTF-8、IDE lint、`git diff --check` 通过;reactor `mvn -o -pl ruoyi-service/ruoyi-order -am install -DskipTests` BUILD SUCCESS;定向 20/20、ruoyi-order 全量 178/178 JUnit 5 全部通过;代码走查通过 | 开始时间依据当天分析记录估算,耗时包含方案分析及等待续作;首轮 Maven 在工作区根目录因 reactor 路径错误未执行构建,进入 `ChargingPile` 后重跑通过 | |
| | | | 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 且勿删除被占用日志文件 | |
| | |
| | | @ApiModelProperty(value = "分配明细:VIN记录ID与本次分配金额", required = true) |
| | | private List<VinManagementAllocateItemDTO> items; |
| | | |
| | | @ApiModelProperty(value = "操作密码", required = true) |
| | | /** |
| | | * 操作密码:VIN码额度分配已取消操作密码校验,该字段仅保留向后兼容,前端无需传值 |
| | | */ |
| | | @ApiModelProperty(value = "操作密码(已废弃,额度分配不再校验)") |
| | | private String operationPassword; |
| | | } |
| | |
| | | @TableField("contact_phone") |
| | | private String contactPhone; |
| | | |
| | | @ApiModelProperty(value = "登录账号(对应sys_user.user_name,roleType=5公司账号)") |
| | | @TableField("account") |
| | | private String account; |
| | | |
| | | @ApiModelProperty(value = "登录密码(前端HMAC-MD5加密值,仅添加时写入,不回显)") |
| | | @TableField("password") |
| | | private String password; |
| | | |
| | | @ApiModelProperty(value = "账户模式(1=预充值,2=后付费)") |
| | | @TableField("account_mode") |
| | | private Integer accountMode; |
| | |
| | | public R<List<SysRole>> getSysRoleByIds(GetSysRoleByIds ids) { |
| | | return R.fail("获取角色失败:" + cause.getMessage()); |
| | | } |
| | | |
| | | @Override |
| | | public R<Long> getRoleIdByRoleKey(String roleKey) { |
| | | return R.fail("获取角色失败:" + cause.getMessage()); |
| | | } |
| | | }; |
| | | } |
| | | } |
| | |
| | | @PostMapping("/role/getSysRoleByIds") |
| | | public R<List<SysRole>> getSysRoleByIds(GetSysRoleByIds ids); |
| | | |
| | | /** |
| | | * 根据角色标识查询启用且未删除的角色ID(公司账号分配默认角色使用) |
| | | */ |
| | | @PostMapping("/role/getRoleIdByRoleKey") |
| | | public R<Long> getRoleIdByRoleKey(String roleKey); |
| | | |
| | | } |
| | |
| | | private static final String SITE_OWNER_ROLE_KEY = "site_owner"; |
| | | private static final String SERVICE_PROVIDER_ROLE_NAME = "服务商"; |
| | | private static final String SERVICE_PROVIDER_ROLE_KEY = "service_provider"; |
| | | private static final String COMPANY_ROLE_NAME = "公司"; |
| | | private static final String COMPANY_ROLE_KEY = "company"; |
| | | private static final String OWN_SITE_NAME = "自有电站"; |
| | | private static final String SYSTEM_ROLE_REMARK = "系统内置角色"; |
| | | |
| | |
| | | ensureSystemRole(INDEPENDENT_PARTNER_ROLE_NAME, INDEPENDENT_PARTNER_ROLE_KEY, getIndependentPartnerMenuIds()); |
| | | ensureSystemRole(SITE_OWNER_ROLE_NAME, SITE_OWNER_ROLE_KEY, new HashSet<>()); |
| | | ensureSystemRole(SERVICE_PROVIDER_ROLE_NAME, SERVICE_PROVIDER_ROLE_KEY, new HashSet<>()); |
| | | ensureSystemRole(COMPANY_ROLE_NAME, COMPANY_ROLE_KEY, new HashSet<>()); |
| | | } |
| | | |
| | | private void ensureSystemRole(String roleName, String roleKey, Set<Long> menuIds) { |
| | |
| | | || SERVICE_PROVIDER_ROLE_NAME.equals(role.getRoleName())); |
| | | } |
| | | |
| | | private boolean isCompanyBuiltinRole(SysRole role) { |
| | | return role != null && (COMPANY_ROLE_KEY.equals(role.getRoleKey()) |
| | | || COMPANY_ROLE_NAME.equals(role.getRoleName())); |
| | | } |
| | | |
| | | private boolean isSystemBuiltinRole(SysRole role) { |
| | | return role != null && (role.isAdmin() || isPartnerBuiltinRole(role)); |
| | | return role != null && (role.isAdmin() || isPartnerBuiltinRole(role) || isCompanyBuiltinRole(role)); |
| | | } |
| | | |
| | | private void fillRoleV2Info(SysRole role) { |
| | |
| | | List<SysRole> sysRoleByIds = roleService.getSysRoleByIds(ids.getIds()); |
| | | return R.ok(sysRoleByIds); |
| | | } |
| | | |
| | | /** |
| | | * 根据角色标识查询启用且未删除的角色ID(远程接口,公司账号分配默认角色使用) |
| | | * |
| | | * @param roleKey 角色标识 |
| | | * @return 角色ID;不存在时返回 null |
| | | */ |
| | | @ResponseBody |
| | | @PostMapping(value = "/getRoleIdByRoleKey") |
| | | public R<Long> getRoleIdByRoleKey(@RequestBody String roleKey) { |
| | | SysRole role = roleService.getOne(new LambdaQueryWrapper<SysRole>() |
| | | .eq(SysRole::getRoleKey, roleKey) |
| | | .eq(SysRole::getDelFlag, 0) |
| | | .eq(SysRole::getStatus, 0) |
| | | .orderByAsc(SysRole::getRoleId) |
| | | .last("LIMIT 1")); |
| | | return R.ok(role == null ? null : role.getRoleId()); |
| | | } |
| | | } |
| | |
| | | if (companySysUserReq.getName() != null && !"".equals(companySysUserReq.getName())) { |
| | | sysUserLambdaQueryWrapper.like(SysUser::getUserName, companySysUserReq.getName()).or().like(SysUser::getPhonenumber, companySysUserReq.getName()); |
| | | } |
| | | sysUserLambdaQueryWrapper.eq(SysUser::getRoleType,2); |
| | | sysUserLambdaQueryWrapper.eq(SysUser::getRoleType,5); |
| | | List<SysUser> list = userService.list(sysUserLambdaQueryWrapper); |
| | | ArrayList<CompanyUserListVo> companyUserListVos = new ArrayList<>(); |
| | | for (SysUser sysUser : list) { |
| | |
| | | @PostMapping("/getSysUserFromCompanyIdOne") |
| | | public R<List<CompanyUserListVo>> getSysUserFromCompanyIdOne(@RequestBody CompanySysUserReq companySysUserReq) { |
| | | LambdaQueryWrapper<SysUser> sysUserLambdaQueryWrapper = new LambdaQueryWrapper<>(); |
| | | sysUserLambdaQueryWrapper.eq(SysUser::getRoleType, 2); |
| | | sysUserLambdaQueryWrapper.eq(SysUser::getRoleType, 5); |
| | | if (companySysUserReq.getName() != null && !"".equals(companySysUserReq.getName())) { |
| | | sysUserLambdaQueryWrapper.like(SysUser::getPhonenumber, companySysUserReq.getName()); |
| | | } |
| | |
| | | List<Long> ids = userService.getSysUserFromPhone(companyUserListVo.getPhoneNumber()); |
| | | SysUser sysUser = new SysUser(); |
| | | if (ids.size() > 0) { |
| | | sysUser.setRoleType(2); |
| | | sysUser.setRoleType(5); |
| | | sysUser.setObjectId(dto.getCompanyId()); |
| | | if(companyUserListVo.getPassword()!=null){ |
| | | sysUser.setPassword(SecurityUtils.encryptPassword(companyUserListVo.getPassword())); |
| | |
| | | userIds.add(ids.get(0).intValue()); |
| | | } else { |
| | | |
| | | sysUser.setRoleType(2); |
| | | sysUser.setRoleType(5); |
| | | sysUser.setObjectId(dto.getCompanyId()); |
| | | if(companyUserListVo.getPassword()!=null){ |
| | | sysUser.setPassword(SecurityUtils.encryptPassword(companyUserListVo.getPassword())); |
| | |
| | | } |
| | | |
| | | @Test |
| | | void doesNothingForCompanyAccount() { |
| | | // roleType=5 为公司账号,不触发合作商站点级角色同步 |
| | | Fixture fixture = new Fixture(2); |
| | | fixture.user.setRoleType(5); |
| | | fixture.user.setObjectId(null); |
| | | |
| | | fixture.service.synchronize(fixture.user); |
| | | |
| | | verify(fixture.siteClient, never()).getPartnerR(any()); |
| | | verify(fixture.roleService, never()).list(any(Wrapper.class)); |
| | | verify(fixture.userRoleService, never()).save(any(SysUserRole.class)); |
| | | } |
| | | |
| | | @Test |
| | | void failsClearlyWhenConfiguredRoleIsMissing() { |
| | | Fixture fixture = new Fixture(3); |
| | | when(fixture.roleService.list(any(Wrapper.class))) |
| | |
| | | @PutMapping public R<Boolean> edit(@RequestBody VinManagementSaveDTO dto) { return R.ok(service.updateVin(dto)); } |
| | | @RequiresPermissions(value = "/vinManagement/delete", logical = Logical.OR) |
| | | @DeleteMapping public R<Boolean> delete(@RequestBody List<Long> ids) { return R.ok(service.deleteVins(ids)); } |
| | | @ApiOperation(value = "批量解绑车队(清空VIN车队归属,返回实际解绑数量)") |
| | | @RequiresPermissions(value = {"/vinManagement/unbind"}, logical = Logical.OR) |
| | | @PutMapping("/unbind-fleet") public R<Integer> unbindFleet(@RequestBody List<Long> ids) { return R.ok(service.unbindFleet(ids)); } |
| | | @RequiresPermissions(value = "/vinManagement/status", logical = Logical.OR) |
| | | @PutMapping("/status") public R<Boolean> status(@RequestBody VinManagementSaveDTO dto) { return R.ok(service.updateStatus(dto.getId(), dto.getStatus())); } |
| | | @RequiresPermissions(value = "/vinManagement/export", logical = Logical.OR) |
| | |
| | | boolean saveVin(VinManagementSaveDTO dto); |
| | | boolean updateVin(VinManagementSaveDTO dto); |
| | | boolean deleteVins(List<Long> ids); |
| | | |
| | | /** |
| | | * 批量解绑车队:仅清空车队归属(fleet_id 置空),公司、初始分配额度、状态等不受影响 |
| | | * |
| | | * @return 实际解绑的VIN数量(未绑定车队的记录不计入) |
| | | */ |
| | | int unbindFleet(List<Long> ids); |
| | | |
| | | boolean updateStatus(Long id, Integer status); |
| | | List<VinManagementExcelDTO> selectExportList(VinManagementQueryDTO query); |
| | | String importVins(List<VinManagementExcelDTO> items); |
| | |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.exception.ServiceException; |
| | | import com.ruoyi.common.core.utils.StringUtils; |
| | | import com.ruoyi.common.security.service.TokenService; |
| | | import com.ruoyi.common.security.utils.SecurityUtils; |
| | | import com.ruoyi.order.api.feignClient.ChargingOrderClient; |
| | | import com.ruoyi.order.api.vo.VinChargingStatusVO; |
| | | 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; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.redisson.api.RLock; |
| | | import org.redisson.api.RedissonClient; |
| | |
| | | |
| | | @Resource |
| | | private GroupCompanyClient groupCompanyClient; |
| | | |
| | | @Resource |
| | | private TokenService tokenService; |
| | | |
| | | @Resource |
| | | private SysUserClient sysUserClient; |
| | | |
| | | @Resource |
| | | private RedissonClient redissonClient; |
| | |
| | | } |
| | | } |
| | | @Override public boolean deleteVins(List<Long> ids) { if (ids == null || ids.isEmpty()) throw new ServiceException("请选择需要删除的VIN码"); List<Long> distinct = new ArrayList<>(new HashSet<>(ids)); if (listByIds(distinct).size() != distinct.size()) throw new ServiceException("部分VIN码不存在或已删除"); return removeByIds(distinct); } |
| | | |
| | | /** |
| | | * 批量解绑车队:仅清空车队归属(fleet_id 置空),公司、初始分配额度、状态等不受影响; |
| | | * 未绑定车队的选中记录忽略不计 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int unbindFleet(List<Long> ids) { |
| | | if (ids == null || ids.isEmpty()) { |
| | | throw new ServiceException("请选择需要解绑车队的VIN码"); |
| | | } |
| | | List<Long> distinct = ids.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); |
| | | if (distinct.isEmpty()) { |
| | | throw new ServiceException("请选择需要解绑车队的VIN码"); |
| | | } |
| | | List<TVinManagement> records = listByIds(distinct); |
| | | if (records.size() != distinct.size()) { |
| | | throw new ServiceException("部分VIN码不存在或已删除"); |
| | | } |
| | | List<Long> boundIds = records.stream() |
| | | .filter(record -> record.getFleetId() != null) |
| | | .map(TVinManagement::getId) |
| | | .collect(Collectors.toList()); |
| | | if (boundIds.isEmpty()) { |
| | | return 0; |
| | | } |
| | | // 显式 set null 清空车队归属(MyBatis-Plus 字段策略会忽略实体 null,需走 UpdateWrapper) |
| | | boolean updated = lambdaUpdate() |
| | | .in(TVinManagement::getId, boundIds) |
| | | .set(TVinManagement::getFleetId, null) |
| | | .set(TVinManagement::getUpdateTime, LocalDateTime.now()) |
| | | .set(TVinManagement::getUpdateBy, SecurityUtils.getUserId()) |
| | | .update(); |
| | | if (!updated) { |
| | | throw new ServiceException("VIN码车队解绑失败,请重试"); |
| | | } |
| | | return boundIds.size(); |
| | | } |
| | | @Override public boolean updateStatus(Long id, Integer status) { if (id == null || !validStatus(status)) throw new ServiceException("VIN码状态参数错误"); if (getById(id) == null) throw new ServiceException("VIN码不存在或已删除"); TVinManagement item = new TVinManagement(); item.setId(id); item.setStatus(status); item.setUpdateTime(LocalDateTime.now()); item.setUpdateBy(SecurityUtils.getUserId()); return updateById(item); } |
| | | @Override public List<VinManagementExcelDTO> selectExportList(VinManagementQueryDTO query) { return baseMapper.selectExportList(query == null ? new VinManagementQueryDTO() : query); } |
| | | @Override public List<TVinManagement> selectAppletVehicleList(Long appUserId) { |
| | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public VinManagementAllocateVO allocateQuota(VinManagementAllocateDTO dto) { |
| | | Map<Long, BigDecimal> amounts = validateAllocateRequest(dto); |
| | | verifyOperationPassword(dto.getOperationPassword()); |
| | | Integer companyId = dto.getCompanyId(); |
| | | // 公司必须存在、启用且为分配模式(共享模式不支持额度分配) |
| | | TGroupCompany company = loadAllocationCompany(companyId); |
| | |
| | | if (dto.getItems().size() > MAX_ALLOCATE_VIN_COUNT) { |
| | | throw new ServiceException("单次最多分配" + MAX_ALLOCATE_VIN_COUNT + "条VIN码"); |
| | | } |
| | | if (StringUtils.isEmpty(dto.getOperationPassword())) { |
| | | throw new ServiceException("请输入操作密码"); |
| | | } |
| | | Map<Long, BigDecimal> amounts = new LinkedHashMap<>(); |
| | | for (VinManagementAllocateItemDTO item : dto.getItems()) { |
| | | if (item == null || item.getVinId() == null) { |
| | |
| | | return "公司可分配余额不足,无法完成本次分配"; |
| | | } |
| | | return "公司可用余额扣减失败:" + msg; |
| | | } |
| | | |
| | | /** 校验操作密码(与卡片管理一致,使用账号操作密码二次确认) */ |
| | | private void verifyOperationPassword(String operationPassword) { |
| | | LoginUser loginUser = tokenService.getLoginUser(); |
| | | if (loginUser == null || loginUser.getUserid() == null) { |
| | | throw new ServiceException("登录状态已失效,请重新登录"); |
| | | } |
| | | R<SysUser> response = sysUserClient.getSysUser(loginUser.getUserid()); |
| | | SysUser user = response == null ? null : response.getData(); |
| | | if (user == null || StringUtils.isEmpty(user.getOperationPassword())) { |
| | | throw new ServiceException("当前账号未设置操作密码"); |
| | | } |
| | | if (!SecurityUtils.matchesPassword(operationPassword, user.getOperationPassword())) { |
| | | throw new ServiceException("操作密码错误"); |
| | | } |
| | | } |
| | | |
| | | /** 抢占并发锁,抢占失败抛出业务异常 */ |
| | |
| | | } |
| | | |
| | | @Test |
| | | void unbindFleetReturnsAffectedCount() { |
| | | TVinManagementService service = mock(TVinManagementService.class); |
| | | when(service.unbindFleet(anyList())).thenReturn(2); |
| | | TVinManagementController controller = buildController(service); |
| | | |
| | | List<Long> ids = Arrays.asList(1L, 2L); |
| | | |
| | | R<Integer> result = controller.unbindFleet(ids); |
| | | |
| | | assertEquals(200, result.getCode()); |
| | | assertEquals(Integer.valueOf(2), result.getData()); |
| | | verify(service).unbindFleet(ids); |
| | | } |
| | | |
| | | @Test |
| | | void statusDelegatesIdAndStatusFromDto() { |
| | | TVinManagementService service = mock(TVinManagementService.class); |
| | | when(service.updateStatus(9L, 2)).thenReturn(true); |
| | |
| | | import com.ruoyi.common.core.context.SecurityContextHolder; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.exception.ServiceException; |
| | | import com.ruoyi.common.security.service.TokenService; |
| | | import com.ruoyi.common.security.utils.SecurityUtils; |
| | | import com.ruoyi.order.api.feignClient.ChargingOrderClient; |
| | | import com.ruoyi.order.api.vo.VinChargingStatusVO; |
| | | 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; |
| | | import org.junit.jupiter.api.AfterEach; |
| | | import org.junit.jupiter.api.BeforeAll; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | |
| | | private GroupCompanyClient groupCompanyClient; |
| | | |
| | | @Mock |
| | | private TokenService tokenService; |
| | | |
| | | @Mock |
| | | private SysUserClient sysUserClient; |
| | | |
| | | @Mock |
| | | private RedissonClient redissonClient; |
| | | |
| | | @Mock |
| | |
| | | ReflectionTestUtils.setField(service, "baseMapper", mapper); |
| | | ReflectionTestUtils.setField(service, "chargingOrderClient", chargingOrderClient); |
| | | ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient); |
| | | ReflectionTestUtils.setField(service, "tokenService", tokenService); |
| | | ReflectionTestUtils.setField(service, "sysUserClient", sysUserClient); |
| | | ReflectionTestUtils.setField(service, "redissonClient", redissonClient); |
| | | lenient().when(redissonClient.getLock(anyString())).thenReturn(quotaLock); |
| | | lenient().when(quotaLock.tryLock(anyLong(), anyLong(), any(TimeUnit.class))).thenReturn(true); |
| | |
| | | assertEquals(2, idsCaptor.getValue().size()); |
| | | assertEquals(Long.valueOf(1L), idsCaptor.getValue().get(0)); |
| | | assertEquals(Long.valueOf(2L), idsCaptor.getValue().get(1)); |
| | | } |
| | | |
| | | // ---------------- unbindFleet ---------------- |
| | | |
| | | @Test |
| | | void unbindFleetRejectsNullOrEmptyIds() { |
| | | ServiceException ex1 = assertThrows(ServiceException.class, () -> service.unbindFleet(null)); |
| | | assertEquals("请选择需要解绑车队的VIN码", ex1.getMessage()); |
| | | |
| | | ServiceException ex2 = assertThrows(ServiceException.class, () -> service.unbindFleet(Collections.emptyList())); |
| | | assertEquals("请选择需要解绑车队的VIN码", ex2.getMessage()); |
| | | |
| | | verifyNoInteractions(mapper); |
| | | } |
| | | |
| | | @Test |
| | | void unbindFleetRejectsMissingRecords() { |
| | | TVinManagement existing = new TVinManagement(); |
| | | existing.setId(1L); |
| | | when(mapper.selectBatchIds(anyCollection())).thenReturn(Collections.singletonList(existing)); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, () -> service.unbindFleet(Arrays.asList(1L, 2L))); |
| | | |
| | | assertEquals("部分VIN码不存在或已删除", ex.getMessage()); |
| | | verify(mapper, never()).update(isNull(), any(Wrapper.class)); |
| | | } |
| | | |
| | | @Test |
| | | void unbindFleetClearsFleetIdAndIgnoresUnboundRecords() { |
| | | TVinManagement bound = new TVinManagement(); |
| | | bound.setId(1L); |
| | | bound.setFleetId(5); |
| | | TVinManagement unbound = new TVinManagement(); |
| | | unbound.setId(2L); |
| | | when(mapper.selectBatchIds(anyCollection())).thenReturn(Arrays.asList(bound, unbound)); |
| | | when(mapper.update(isNull(), any(Wrapper.class))).thenReturn(1); |
| | | |
| | | int result = service.unbindFleet(Arrays.asList(1L, 2L, 2L, null)); |
| | | |
| | | assertEquals(1, result); |
| | | @SuppressWarnings("unchecked") |
| | | ArgumentCaptor<List<Long>> idsCaptor = ArgumentCaptor.forClass(List.class); |
| | | verify(mapper).selectBatchIds(idsCaptor.capture()); |
| | | assertEquals(Arrays.asList(1L, 2L), idsCaptor.getValue()); |
| | | ArgumentCaptor<Wrapper> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class); |
| | | verify(mapper).update(isNull(), wrapperCaptor.capture()); |
| | | String sqlSet = wrapperCaptor.getValue().getSqlSet(); |
| | | assertTrue(sqlSet.contains("fleet_id=")); |
| | | assertTrue(sqlSet.contains("update_time=")); |
| | | assertTrue(sqlSet.contains("update_by=")); |
| | | } |
| | | |
| | | // ---------------- updateStatus ---------------- |
| | |
| | | |
| | | @Test |
| | | void allocateQuotaAccumulatesVinQuotaAndDeductsCompanyBalance() { |
| | | mockOperationPassword(); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | doReturn(Arrays.asList(allocateVehicle(1L, 9, "VIN-1", "10.00"), allocateVehicle(2L, 9, "VIN-2", "20.00"))) |
| | | .when(service).listByIds(anyCollection()); |
| | |
| | | when(mapper.addInitQuota(1L, new BigDecimal("10.00"), CURRENT_USER)).thenReturn(1); |
| | | when(mapper.addInitQuota(2L, new BigDecimal("20.00"), CURRENT_USER)).thenReturn(1); |
| | | |
| | | VinManagementAllocateVO vo = service.allocateQuota(allocateDto(9, "pwd", |
| | | VinManagementAllocateVO vo = service.allocateQuota(allocateDto(9, |
| | | allocateItem(1L, "10.00"), allocateItem(2L, "20.00"))); |
| | | |
| | | assertEquals(new BigDecimal("970.00"), vo.getAvailableBalance()); |
| | |
| | | } |
| | | |
| | | @Test |
| | | void allocateQuotaRejectsWrongOperationPassword() { |
| | | mockOperationPassword(); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, "bad", allocateItem(1L, "10.00")))); |
| | | |
| | | assertEquals("操作密码错误", exception.getMessage()); |
| | | verifyNoInteractions(groupCompanyClient); |
| | | } |
| | | |
| | | @Test |
| | | void allocateQuotaRejectsSharedModeCompany() { |
| | | mockOperationPassword(); |
| | | TGroupCompany shared = allocationCompany(9, "共享公司"); |
| | | shared.setAccountMode(1); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(shared)); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, "pwd", allocateItem(1L, "10.00")))); |
| | | () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00")))); |
| | | |
| | | assertEquals("该公司为共享模式,不支持额度分配", exception.getMessage()); |
| | | verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class)); |
| | |
| | | |
| | | @Test |
| | | void allocateQuotaRejectsVehicleOfOtherCompany() { |
| | | mockOperationPassword(); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | doReturn(Collections.singletonList(allocateVehicle(1L, 8, "VIN-1", "0.00"))) |
| | | .when(service).listByIds(anyCollection()); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, "pwd", allocateItem(1L, "10.00")))); |
| | | () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00")))); |
| | | |
| | | assertTrue(exception.getMessage().contains("不属于所选公司")); |
| | | verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class)); |
| | |
| | | |
| | | @Test |
| | | void allocateQuotaRejectsDisabledVehicle() { |
| | | mockOperationPassword(); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | TVinManagement vehicle = allocateVehicle(1L, 9, "VIN-1", "0.00"); |
| | | vehicle.setStatus(2); |
| | | doReturn(Collections.singletonList(vehicle)).when(service).listByIds(anyCollection()); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, "pwd", allocateItem(1L, "10.00")))); |
| | | () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00")))); |
| | | |
| | | assertTrue(exception.getMessage().contains("已禁用")); |
| | | verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class)); |
| | |
| | | |
| | | @Test |
| | | void allocateQuotaRejectsQuotaOverflow() { |
| | | mockOperationPassword(); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | doReturn(Collections.singletonList(allocateVehicle(1L, 9, "VIN-1", "99999999.00"))) |
| | | .when(service).listByIds(anyCollection()); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, "pwd", allocateItem(1L, "100.00")))); |
| | | () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "100.00")))); |
| | | |
| | | assertTrue(exception.getMessage().contains("超过上限")); |
| | | verify(groupCompanyClient, never()).deductBalance(any(Integer.class), any(BigDecimal.class)); |
| | |
| | | |
| | | @Test |
| | | void allocateQuotaRefundsCompanyBalanceWhenQuotaUpdateFails() { |
| | | mockOperationPassword(); |
| | | when(groupCompanyClient.getCompanyById(9)).thenReturn(R.ok(allocationCompany(9, "测试公司"))); |
| | | doReturn(Collections.singletonList(allocateVehicle(1L, 9, "VIN-1", "10.00"))) |
| | | .when(service).listByIds(anyCollection()); |
| | |
| | | when(groupCompanyClient.refundBalance(9, new BigDecimal("10.00"))).thenReturn(R.ok(new BigDecimal("1000.00"))); |
| | | |
| | | ServiceException exception = assertThrows(ServiceException.class, |
| | | () -> service.allocateQuota(allocateDto(9, "pwd", allocateItem(1L, "10.00")))); |
| | | () -> service.allocateQuota(allocateDto(9, allocateItem(1L, "10.00")))); |
| | | |
| | | assertEquals("VIN码【VIN-1】额度分配失败,请重试", exception.getMessage()); |
| | | verify(groupCompanyClient).refundBalance(9, new BigDecimal("10.00")); |
| | | } |
| | | |
| | | private void mockOperationPassword() { |
| | | LoginUser loginUser = new LoginUser(); |
| | | loginUser.setUserid(CURRENT_USER); |
| | | SysUser user = new SysUser(); |
| | | user.setOperationPassword(SecurityUtils.encryptPassword("pwd")); |
| | | when(tokenService.getLoginUser()).thenReturn(loginUser); |
| | | when(sysUserClient.getSysUser(CURRENT_USER)).thenReturn(R.ok(user)); |
| | | } |
| | | |
| | | private TGroupCompany allocationCompany(Integer id, String name) { |
| | |
| | | return item; |
| | | } |
| | | |
| | | private VinManagementAllocateDTO allocateDto(Integer companyId, String password, VinManagementAllocateItemDTO... items) { |
| | | private VinManagementAllocateDTO allocateDto(Integer companyId, VinManagementAllocateItemDTO... items) { |
| | | VinManagementAllocateDTO dto = new VinManagementAllocateDTO(); |
| | | dto.setCompanyId(companyId); |
| | | dto.setItems(Arrays.asList(items)); |
| | | dto.setOperationPassword(password); |
| | | return dto; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * 添加公司 |
| | | * 添加公司(同步创建登录账号,roleType=5) |
| | | */ |
| | | @RequiresPermissions(value = {"/companyManagement/list"}, logical = Logical.OR) |
| | | @PostMapping("/add") |
| | |
| | | if (!StringUtils.hasText(company.getName())) { |
| | | return AjaxResult.error("公司名称不能为空"); |
| | | } |
| | | if (!StringUtils.hasText(company.getAccount())) { |
| | | return AjaxResult.error("登录账号不能为空"); |
| | | } |
| | | if (!StringUtils.hasText(company.getPassword())) { |
| | | return AjaxResult.error("登录密码不能为空"); |
| | | } |
| | | company.setId(null); |
| | | company.setDelFlag(false); |
| | | if (company.getStatus() == null) { |
| | | company.setStatus(1); |
| | | } |
| | | groupCompanyService.save(company); |
| | | return AjaxResult.success(); |
| | | return groupCompanyService.addCompany(company); |
| | | } |
| | | |
| | | /** |
| | | * 编辑公司 |
| | | * 编辑公司(同步登录账号资料,密码不可修改) |
| | | */ |
| | | @RequiresPermissions(value = {"/companyManagement/list"}, logical = Logical.OR) |
| | | @PostMapping("/edit") |
| | |
| | | if (company.getId() == null) { |
| | | return AjaxResult.error("公司ID不能为空"); |
| | | } |
| | | groupCompanyService.updateById(company); |
| | | return AjaxResult.success(); |
| | | return groupCompanyService.editCompany(company); |
| | | } |
| | | |
| | | /** |
| | | * 删除公司 |
| | | * 删除公司(同步停用其登录账号 delFlag=2) |
| | | */ |
| | | @RequiresPermissions(value = {"/companyManagement/list"}, logical = Logical.OR) |
| | | @DeleteMapping("/del") |
| | |
| | | if (id == null) { |
| | | return AjaxResult.error("公司ID不能为空"); |
| | | } |
| | | groupCompanyService.removeById(id); |
| | | return AjaxResult.success(); |
| | | return groupCompanyService.delCompany(id); |
| | | } |
| | | |
| | | /** |
| | |
| | | package com.ruoyi.other.service; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.ruoyi.common.core.web.domain.AjaxResult; |
| | | import com.ruoyi.other.api.domain.TGroupCompany; |
| | | |
| | | /** |
| | |
| | | */ |
| | | TGroupCompany getDetail(Integer id); |
| | | |
| | | /** |
| | | * 添加公司并同步创建登录账号(sys_user,roleType=5) |
| | | * |
| | | * @param company 公司(含账号、密码) |
| | | * @return 操作结果 |
| | | */ |
| | | AjaxResult addCompany(TGroupCompany company); |
| | | |
| | | /** |
| | | * 编辑公司并同步登录账号资料(不修改密码) |
| | | * |
| | | * @param company 公司 |
| | | * @return 操作结果 |
| | | */ |
| | | AjaxResult editCompany(TGroupCompany company); |
| | | |
| | | /** |
| | | * 删除公司并停用其登录账号(delFlag=2) |
| | | * |
| | | * @param id 公司ID |
| | | * @return 操作结果 |
| | | */ |
| | | AjaxResult delCompany(Integer id); |
| | | |
| | | } |
| | |
| | | |
| | | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.utils.StringUtils; |
| | | import com.ruoyi.common.core.web.domain.AjaxResult; |
| | | import com.ruoyi.other.api.domain.TFleet; |
| | | import com.ruoyi.other.api.domain.TGroupCompany; |
| | | import com.ruoyi.other.api.model.TCompanyRechargeRecord; |
| | |
| | | import com.ruoyi.other.mapper.TFleetMapper; |
| | | import com.ruoyi.other.mapper.TGroupCompanyMapper; |
| | | import com.ruoyi.other.service.TGroupCompanyService; |
| | | import com.ruoyi.system.api.domain.SysUser; |
| | | import com.ruoyi.system.api.feignClient.SysRoleClient; |
| | | import com.ruoyi.system.api.feignClient.SysUserClient; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.Objects; |
| | | |
| | |
| | | /** 金额保留小数位(与 t_company_recharge_record.arrival_amount 一致) */ |
| | | private static final int AMOUNT_SCALE = 2; |
| | | |
| | | /** 公司账号角色标识(内置角色 role_key=company) */ |
| | | private static final String COMPANY_ROLE_KEY = "company"; |
| | | |
| | | /** 公司账号角色类型(sys_user.roleType=5) */ |
| | | private static final int COMPANY_ROLE_TYPE = 5; |
| | | |
| | | /** |
| | | * 车队 Mapper。 |
| | | * 此处注入 Mapper 而非 TFleetService:TFleetServiceImpl 已依赖 TGroupCompanyService, |
| | |
| | | /** 公司充值记录 Mapper */ |
| | | @Resource |
| | | private TCompanyRechargeRecordMapper companyRechargeRecordMapper; |
| | | |
| | | /** 系统用户远程客户端(公司登录账号同步) */ |
| | | @Resource |
| | | private SysUserClient sysUserClient; |
| | | |
| | | /** 系统角色远程客户端(公司默认角色查询) */ |
| | | @Resource |
| | | private SysRoleClient sysRoleClient; |
| | | |
| | | @Override |
| | | public TGroupCompany getDetail(Integer id) { |
| | |
| | | .setScale(AMOUNT_SCALE, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | /** |
| | | * 查询公司默认角色ID(内置角色 role_key=company) |
| | | */ |
| | | private Long resolveCompanyRoleId() { |
| | | R<Long> roleR = sysRoleClient.getRoleIdByRoleKey(COMPANY_ROLE_KEY); |
| | | if (roleR == null || 200 != roleR.getCode() || roleR.getData() == null) { |
| | | throw new RuntimeException("系统内置角色【" + COMPANY_ROLE_KEY + "】不存在或已停用,请先配置角色"); |
| | | } |
| | | return roleR.getData(); |
| | | } |
| | | |
| | | /** |
| | | * 检查登录账号在 t_group_company 中是否已存在(编辑时排除自身) |
| | | */ |
| | | private boolean accountExists(String account, Integer excludeId) { |
| | | Long count = count(Wrappers.<TGroupCompany>lambdaQuery() |
| | | .eq(TGroupCompany::getAccount, account) |
| | | .ne(excludeId != null, TGroupCompany::getId, excludeId)); |
| | | return count != null && count > 0; |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public AjaxResult addCompany(TGroupCompany company) { |
| | | if (!StringUtils.hasText(company.getAccount())) { |
| | | return AjaxResult.error("登录账号不能为空"); |
| | | } |
| | | if (!StringUtils.hasText(company.getPassword())) { |
| | | return AjaxResult.error("登录密码不能为空"); |
| | | } |
| | | if (accountExists(company.getAccount(), null)) { |
| | | return AjaxResult.error("登录账号不能重复"); |
| | | } |
| | | R<SysUser> existR = sysUserClient.queryUserByUserName(company.getAccount()); |
| | | SysUser existUser = existR == null ? null : existR.getData(); |
| | | if (existUser != null) { |
| | | return AjaxResult.error("登录账号不能重复"); |
| | | } |
| | | Long roleId = resolveCompanyRoleId(); |
| | | SysUser user = new SysUser(); |
| | | user.setUserName(company.getAccount()); |
| | | user.setNickName(company.getName()); |
| | | user.setPhonenumber(company.getContactPhone()); |
| | | user.setPassword(company.getPassword()); |
| | | user.setStatus("0"); |
| | | user.setDelFlag("0"); |
| | | user.setRoleId(roleId); |
| | | user.setRoleType(COMPANY_ROLE_TYPE); |
| | | user.setPassWordUpdate(new Date()); |
| | | R<Long> r = sysUserClient.addSysUser(user); |
| | | if (r == null || 200 != r.getCode()) { |
| | | throw new RuntimeException(r == null ? "创建登录账号失败" : r.getMsg()); |
| | | } |
| | | save(company); |
| | | // 回写 objectId=公司ID(账号创建时公司尚未保存) |
| | | R<SysUser> createdR = sysUserClient.getSysUser(r.getData()); |
| | | if (createdR != null && 200 == createdR.getCode() && createdR.getData() != null) { |
| | | SysUser created = createdR.getData(); |
| | | created.setObjectId(company.getId()); |
| | | sysUserClient.updateUser(created); |
| | | } |
| | | return AjaxResult.success(); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public AjaxResult editCompany(TGroupCompany company) { |
| | | TGroupCompany old = getById(company.getId()); |
| | | if (old == null) { |
| | | return AjaxResult.error("公司不存在"); |
| | | } |
| | | // 编辑时账号只读(前端禁用),此处兼容外部调用方传入账号的场景:唯一性校验(排除自身) |
| | | if (StringUtils.hasText(company.getAccount())) { |
| | | if (accountExists(company.getAccount(), company.getId())) { |
| | | return AjaxResult.error("登录账号不能重复"); |
| | | } |
| | | } |
| | | String account = StringUtils.hasText(company.getAccount()) ? company.getAccount() : old.getAccount(); |
| | | if (StringUtils.hasText(account)) { |
| | | syncCompanyUserOnEdit(company, account); |
| | | } |
| | | updateById(company); |
| | | return AjaxResult.success(); |
| | | } |
| | | |
| | | /** |
| | | * 编辑公司时同步登录账号:存在则仅更新账号资料(不修改密码),不存在则补建 |
| | | */ |
| | | private void syncCompanyUserOnEdit(TGroupCompany company, String account) { |
| | | R<SysUser> userR = sysUserClient.queryUserByUserName(account); |
| | | SysUser user = userR == null ? null : userR.getData(); |
| | | if (user == null) { |
| | | if (!StringUtils.hasText(company.getPassword())) { |
| | | throw new RuntimeException("补建登录账号时登录密码不能为空"); |
| | | } |
| | | Long roleId = resolveCompanyRoleId(); |
| | | SysUser newUser = new SysUser(); |
| | | newUser.setUserName(account); |
| | | newUser.setNickName(company.getName()); |
| | | newUser.setPhonenumber(company.getContactPhone()); |
| | | newUser.setPassword(company.getPassword()); |
| | | newUser.setStatus("0"); |
| | | newUser.setDelFlag("0"); |
| | | newUser.setRoleId(roleId); |
| | | newUser.setRoleType(COMPANY_ROLE_TYPE); |
| | | newUser.setObjectId(company.getId()); |
| | | newUser.setPassWordUpdate(new Date()); |
| | | R<Long> r = sysUserClient.addSysUser(newUser); |
| | | if (r == null || 200 != r.getCode()) { |
| | | throw new RuntimeException(r == null ? "创建登录账号失败" : r.getMsg()); |
| | | } |
| | | return; |
| | | } |
| | | // 远程查询返回的密码字段可能是哈希或脱敏值,显式清空避免再次写回 |
| | | user.setPassword(null); |
| | | user.setPassWordUpdate(null); |
| | | user.setUserName(account); |
| | | user.setNickName(company.getName()); |
| | | user.setPhonenumber(company.getContactPhone()); |
| | | R<Boolean> r = sysUserClient.updateSysUser(user); |
| | | if (r == null || 200 != r.getCode()) { |
| | | throw new RuntimeException(r == null ? "更新登录账号失败" : r.getMsg()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public AjaxResult delCompany(Integer id) { |
| | | TGroupCompany company = getById(id); |
| | | if (company == null) { |
| | | return AjaxResult.error("公司不存在"); |
| | | } |
| | | removeById(id); |
| | | if (StringUtils.hasText(company.getAccount())) { |
| | | R<SysUser> userR = sysUserClient.queryUserByUserName(company.getAccount()); |
| | | SysUser user = userR == null ? null : userR.getData(); |
| | | if (user != null) { |
| | | user.setDelFlag("2"); |
| | | sysUserClient.updateUser(user); |
| | | } |
| | | } |
| | | return AjaxResult.success(); |
| | | } |
| | | |
| | | } |
| | |
| | | private EmindEnergyChainMapper mapper; |
| | | @Mock |
| | | private EmindStationMapper stationMapper; |
| | | @Mock |
| | | private EmindEnergyChainDetailService emindEnergyChainDetailService; |
| | | private EmindEnergyChainManagementService service; |
| | | |
| | | @BeforeEach |
| | | void setUp() { |
| | | Clock clock = Clock.fixed(Instant.parse("2026-07-25T08:00:00Z"), ZoneOffset.UTC); |
| | | service = new EmindEnergyChainManagementService(mapper, stationMapper, clock); |
| | | service = new EmindEnergyChainManagementService(mapper, stationMapper, clock, emindEnergyChainDetailService); |
| | | } |
| | | |
| | | @Test |
| | |
| | | buildService(company, fleetMapper, rechargeMapper).getDetail(2); |
| | | |
| | | ArgumentCaptor<Wrapper<TCompanyRechargeRecord>> captor = ArgumentCaptor.forClass(Wrapper.class); |
| | | Mockito.verify(rechargeMapper).selectList(captor.capture()); |
| | | // getDetail 会查询两次充值记录:本月 + 累计;首次调用为本月查询 |
| | | Mockito.verify(rechargeMapper, Mockito.times(2)).selectList(captor.capture()); |
| | | // 过滤条件:company_id = 2,且 recharge_time ∈ [本月1日 00:00:00, 次月1日 00:00:00) |
| | | String sqlSegment = captor.getValue().getSqlSegment(); |
| | | String sqlSegment = captor.getAllValues().get(0).getSqlSegment(); |
| | | assertTrue(sqlSegment.contains("company_id"), sqlSegment); |
| | | assertTrue(sqlSegment.contains("recharge_time"), sqlSegment); |
| | | |
| | | LocalDateTime start = TGroupCompanyServiceImpl.monthStart(LocalDate.now()); |
| | | Map<?, ?> paramValues = (Map<?, ?>) ReflectionTestUtils.getField(captor.getValue(), "paramNameValuePairs"); |
| | | Map<?, ?> paramValues = (Map<?, ?>) ReflectionTestUtils.getField(captor.getAllValues().get(0), "paramNameValuePairs"); |
| | | assertTrue(paramValues != null && paramValues.containsValue(2), String.valueOf(paramValues)); |
| | | assertTrue(paramValues != null && paramValues.containsValue(start), String.valueOf(paramValues)); |
| | | assertTrue(paramValues != null && paramValues.containsValue(start.plusMonths(1)), String.valueOf(paramValues)); |
| New file |
| | |
| | | -- ------------------------------------------ |
| | | -- 公司管理:登录账号/密码字段 + 内置公司角色 + sys_user.roleType 注释 |
| | | -- 创建日期: 2026-09-28 |
| | | -- 说明: |
| | | -- 1. t_group_company 新增 account/password(存储前端 HMAC-MD5 加密后的登录密码) |
| | | -- 2. sys_role 幂等补充内置角色 role_key=company(所有公司账号共享,菜单在角色管理配置) |
| | | -- 3. sys_user.roleType 注释统一为 1=平台 2=合作商 3=门店 4=修理厂 5=公司 |
| | | -- 库: charging_pile_other |
| | | -- ------------------------------------------ |
| | | |
| | | -- 1. t_group_company 增加登录账号/密码字段(幂等) |
| | | SET @gc_account_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = DATABASE() |
| | | AND TABLE_NAME = 't_group_company' |
| | | AND COLUMN_NAME = 'account' |
| | | ); |
| | | |
| | | SET @gc_account_ddl := IF( |
| | | @gc_account_exists = 0, |
| | | 'ALTER TABLE `t_group_company` ADD COLUMN `account` varchar(255) NULL DEFAULT NULL COMMENT ''登录账号'' AFTER `contact_phone`', |
| | | 'SELECT 1' |
| | | ); |
| | | |
| | | PREPARE gc_account_stmt FROM @gc_account_ddl; |
| | | EXECUTE gc_account_stmt; |
| | | DEALLOCATE PREPARE gc_account_stmt; |
| | | |
| | | SET @gc_password_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = DATABASE() |
| | | AND TABLE_NAME = 't_group_company' |
| | | AND COLUMN_NAME = 'password' |
| | | ); |
| | | |
| | | SET @gc_password_ddl := IF( |
| | | @gc_password_exists = 0, |
| | | 'ALTER TABLE `t_group_company` ADD COLUMN `password` varchar(255) NULL DEFAULT NULL COMMENT ''登录密码(前端HMAC-MD5加密值)'' AFTER `account`', |
| | | 'SELECT 1' |
| | | ); |
| | | |
| | | PREPARE gc_password_stmt FROM @gc_password_ddl; |
| | | EXECUTE gc_password_stmt; |
| | | DEALLOCATE PREPARE gc_password_stmt; |
| | | |
| | | -- 2. 内置公司角色(幂等) |
| | | INSERT INTO sys_role (role_name, role_key, data_scope, status, del_flag, create_by, create_time, remark) |
| | | SELECT '公司', 'company', '5', '0', '0', 'admin', NOW(), '系统内置角色' |
| | | WHERE NOT EXISTS ( |
| | | SELECT 1 FROM sys_role |
| | | WHERE del_flag = '0' AND (role_name = '公司' OR role_key = 'company') |
| | | ); |
| | | |
| | | UPDATE sys_role |
| | | SET role_name = '公司', |
| | | role_key = 'company', |
| | | status = '0', |
| | | del_flag = '0', |
| | | remark = '系统内置角色', |
| | | update_by = 'admin', |
| | | update_time = NOW() |
| | | WHERE role_name = '公司' OR role_key = 'company'; |
| | | |
| | | -- 3. sys_user.roleType 字段注释统一(5=公司) |
| | | ALTER TABLE `sys_user` |
| | | MODIFY COLUMN `roleType` int NULL DEFAULT 1 COMMENT '角色类型 1=平台 2=合作商 3=门店 4=修理厂 5=公司'; |
| New file |
| | |
| | | -- VIN码管理-批量解绑 按钮权限增量脚本 |
| | | -- 说明:本功能无表结构变更(复用 t_vin_management), |
| | | -- 仅在 charging_pile_other 逻辑库新增按钮权限。 |
| | | -- 幂等:已存在同名 perms 时自动跳过,可重复执行。 |
| | | |
| | | USE charging_pile_other; |
| | | |
| | | -- 批量解绑按钮权限;父菜单按现有VIN码管理菜单定位(历史数据可能存在多个VIN码管理节点,逐一定位均已覆盖)。 |
| | | INSERT INTO `sys_menu` |
| | | (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) |
| | | SELECT permissions.menu_name, parent.menu_id, permissions.order_num, '#', '', '', 1, 0, 'F', '0', '0', permissions.perms, '#', 'admin', NOW(), '', NULL, '' |
| | | FROM `sys_menu` parent |
| | | JOIN ( |
| | | SELECT '批量解绑' AS menu_name, 10 AS order_num, '/vinManagement/unbind' AS perms |
| | | ) permissions |
| | | WHERE parent.perms = '/vinManagement/list' |
| | | AND NOT EXISTS ( |
| | | SELECT 1 FROM `sys_menu` existing WHERE existing.perms = permissions.perms |
| | | ); |
| | | |
| | | -- 默认仅授予管理员角色;其他角色由后台按职责单独授权。 |
| | | INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) |
| | | SELECT 1, child.menu_id |
| | | FROM `sys_menu` child |
| | | WHERE child.parent_id = ( |
| | | SELECT parent.menu_id FROM `sys_menu` parent WHERE parent.perms = '/vinManagement/list' LIMIT 1 |
| | | ) |
| | | AND child.perms = '/vinManagement/unbind' |
| | | AND NOT EXISTS ( |
| | | SELECT 1 FROM `sys_role_menu` existing |
| | | WHERE existing.role_id = 1 AND existing.menu_id = child.menu_id |
| | | ); |