xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/controller/XpOperationsRoleController.java
@@ -6,9 +6,7 @@ import com.xiaopeng.api.model.account.XpOperationsRole; import com.xiaopeng.common.core.domain.R; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -43,5 +41,17 @@ return R.ok(list); } /** * 内部调用 * 查询运维人员关联的角色列表 * * @param operationUserId 运维人员id */ @GetMapping("/inner/xp-operations-role/listByOperationUserId") public R<List<XpOperationsRole>> listByOperationUserId(@RequestParam("operationUserId") Long operationUserId) { List<XpOperationsRole> list = xpOperationsRoleService.list(new QueryWrapper<XpOperationsRole>() .eq("operations_user_id", operationUserId)); return R.ok(list); } } xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/account/XpOperationsRoleClientFallbackFactory.java
New file @@ -0,0 +1,25 @@ package com.xiaopeng.api.factory.account; import com.xiaopeng.api.feignClient.account.XpOperationsRoleClient; import com.xiaopeng.api.model.account.XpOperationsRole; import com.xiaopeng.common.core.domain.R; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.openfeign.FallbackFactory; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; @Slf4j @Component public class XpOperationsRoleClientFallbackFactory implements FallbackFactory<XpOperationsRoleClient> { @Override public XpOperationsRoleClient create(Throwable cause) { return operationUserId -> { log.error("查询运维人员角色关联失败,operationUserId={}, err={}", operationUserId, cause.getMessage()); return R.ok(new ArrayList<>()); }; } } xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/account/XpOperationsRoleClient.java
New file @@ -0,0 +1,31 @@ package com.xiaopeng.api.feignClient.account; import com.xiaopeng.api.factory.account.XpOperationsRoleClientFallbackFactory; import com.xiaopeng.api.model.account.XpOperationsRole; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.dto.ServiceNameConstantsEnum; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; /** * 运维人员角色关联 Feign 客户端 */ @FeignClient( contextId = "XpOperationsRoleClient", value = ServiceNameConstantsEnum.XP_ACCOUNT_SERVICE, fallbackFactory = XpOperationsRoleClientFallbackFactory.class ) public interface XpOperationsRoleClient { /** * 根据运维人员ID查询运维人员-角色关联列表 * * @param operationUserId 运维人员id */ @GetMapping("/inner/xp-operations-role/listByOperationUserId") R<List<XpOperationsRole>> listByOperationUserId(@RequestParam("operationUserId") Long operationUserId); } xp-robotaxi-api/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1,5 +1,6 @@ com.xiaopeng.api.factory.operation.SysLogClientFallbackFactory com.xiaopeng.api.factory.account.XpAppUserCouponClientFallbackFactory com.xiaopeng.api.factory.account.XpOperationsRoleClientFallbackFactory com.xiaopeng.api.factory.account.XpAppUserClientFallbackFactory com.xiaopeng.api.factory.map.XpMapClientFallbackFactory com.xiaopeng.api.factory.operation.RemoteMenuFallbackFactory xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/constant/I18nKeyConstant.java
@@ -86,4 +86,15 @@ String CRUISE_PLAN_ADD_FAIL = "cruise.plan.add.fail"; // 编辑巡游计划失败 String CRUISE_PLAN_EDIT_FAIL = "cruise.plan.edit.fail"; // 点位-运营区域ID不能为空 String POINT_ELECTRONIC_AREA_ID_EMPTY = "point.electronic.area.id.empty"; // 点位-运营区域不存在 String POINT_ELECTRONIC_AREA_NOT_EXIST = "point.electronic.area.not.exist"; // 点位-城市信息不存在 String POINT_CITY_NOT_EXIST = "point.city.not.exist"; // 点位-编号锁获取失败 String POINT_NUMBER_LOCK_FAIL = "point.number.lock.fail"; // 点位-编号解析失败 String POINT_NUMBER_PARSE_FAIL = "point.number.parse.fail"; } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpPointManagementController.java
@@ -89,9 +89,7 @@ // @Transactional // @ShardingSphereTransactionType(TransactionType.BASE) public R add(@Validated @RequestBody PointAddDto pointAddDto) { XpPoint xpPoint = new XpPoint(); BeanUtils.copyProperties(pointAddDto, xpPoint); xpPointService.save(xpPoint); xpPointService.addPointWithGeneratedNumber(pointAddDto); return R.ok(); } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/mapper/XpPointMapper.java
@@ -9,6 +9,7 @@ import com.xiaopeng.api.vo.order.XpPointsListVo; import com.xiaopeng.common.core.web.page.PageInfo; import com.xiaopeng.map.query.PointByNameQuery; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Param; import java.util.List; @@ -31,4 +32,7 @@ List<XpPointsListVo> queryPointByName(@Param("query") PointByNameQuery query); List<XpPoint> pageList(@Param("pageInfo") PageInfo<XpPoint> pageInfo, @Param("query") PointPageListListQuery query); @Select("SELECT point_number FROM xp_point WHERE point_number LIKE CONCAT(#{cityPrefix}, '-', #{bizType}, '-%') ORDER BY point_number DESC LIMIT 1") String selectMaxPointNumber(@Param("cityPrefix") String cityPrefix, @Param("bizType") String bizType); } xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpPointService.java
@@ -2,6 +2,7 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.xiaopeng.api.dto.operation.PointAddDto; import com.xiaopeng.api.dto.operation.PointMapQuery; import com.xiaopeng.api.dto.order.HistoryEndPointsDto; import com.xiaopeng.api.dto.order.HistoryStartPointsDto; @@ -33,6 +34,11 @@ public interface XpPointService extends IService<XpPoint> { /** * 新增点位并根据规则生成点位编号 */ void addPointWithGeneratedNumber(@Valid PointAddDto pointAddDto); /** * 获取附近上车站点 */ NearestVo nearest(@Valid NearestDto dto); xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java
@@ -3,10 +3,13 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.xiaopeng.api.dto.operation.PointAddDto; import com.xiaopeng.api.dto.operation.PointMapQuery; import com.xiaopeng.api.dto.order.HistoryEndPointsDto; import com.xiaopeng.api.dto.order.HistoryStartPointsDto; import com.xiaopeng.api.model.operation.XpElectronicFenceArea; import com.xiaopeng.api.model.operation.XpPoint; import com.xiaopeng.api.model.operation.XpRegion; import com.xiaopeng.api.model.operation.XpSystemSetting; import com.xiaopeng.api.query.operation.PointListQuery; import com.xiaopeng.api.query.operation.PointPageListListQuery; @@ -15,12 +18,17 @@ import com.xiaopeng.api.vo.order.XpPointsListVo; import com.xiaopeng.api.vo.order.XpPointsSearchVo; import com.xiaopeng.common.core.constant.CacheConstants; import com.xiaopeng.common.core.exception.ServiceException; import com.xiaopeng.common.core.utils.StringUtils; import com.xiaopeng.common.core.web.page.PageInfo; import com.xiaopeng.common.redis.service.RedisService; import com.xiaopeng.map.constant.I18nKeyConstant; import com.xiaopeng.map.dto.NearestDto; import com.xiaopeng.map.mapper.XpPointMapper; import com.xiaopeng.map.mapper.XpRegionMapper; import com.xiaopeng.map.mapper.XpSystemSettingMapper; import com.xiaopeng.map.query.PointByNameQuery; import com.xiaopeng.map.service.XpElectronicFenceAreaService; import com.xiaopeng.map.service.XpPointService; import com.xiaopeng.map.utils.TimeRangeChecker; import com.xiaopeng.map.utils.holiday.HolidayUtil; @@ -28,6 +36,12 @@ import com.xiaopeng.map.vo.NearestNextVo; import com.xiaopeng.map.vo.NearestVo; import com.xiaopeng.map.vo.OrderDistanceVo; import lombok.extern.slf4j.Slf4j; import net.sourceforge.pinyin4j.PinyinHelper; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.Resource; @@ -35,6 +49,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** @@ -47,7 +62,13 @@ * @since 2026-02-03 */ @Service @Slf4j public class XpPointServiceImpl extends ServiceImpl<XpPointMapper, XpPoint> implements XpPointService { private static final String POINT_BIZ_TYPE = "PUD"; private static final int SERIAL_WIDTH = 6; private static final int LOCK_WAIT_SECONDS = 5; private static final int LOCK_LEASE_SECONDS = 30; @Resource private XpSystemSettingMapper xpSystemSettingMapper; @@ -58,6 +79,59 @@ @Resource private HolidayUtil holidayUtil; @Resource private XpElectronicFenceAreaService xpElectronicFenceAreaService; @Resource private XpRegionMapper xpRegionMapper; @Autowired private RedissonClient redissonClient; @Override public void addPointWithGeneratedNumber(PointAddDto pointAddDto) { if (pointAddDto == null) { throw new ServiceException("参数不能为空", I18nKeyConstant.SYSTEM_EXCEPTION); } if (pointAddDto.getElectronicAreaId() == null) { throw new ServiceException("运营区域ID不能为空", I18nKeyConstant.POINT_ELECTRONIC_AREA_ID_EMPTY); } String cityPrefix = buildCityPrefix(pointAddDto.getElectronicAreaId()); String lockKey = buildPointNumberLockKey(cityPrefix, POINT_BIZ_TYPE); RLock lock = redissonClient.getLock(lockKey); boolean locked = false; String pointNumber; try { locked = lock.tryLock(LOCK_WAIT_SECONDS, LOCK_LEASE_SECONDS, TimeUnit.SECONDS); if (!locked) { throw new ServiceException("点位编号生成繁忙,请稍后重试", I18nKeyConstant.POINT_NUMBER_LOCK_FAIL); } String maxPointNumber = this.baseMapper.selectMaxPointNumber(cityPrefix, POINT_BIZ_TYPE); int nextSerial = 1; if (StringUtils.hasText(maxPointNumber)) { nextSerial = parseSerialOrThrow(maxPointNumber) + 1; } pointNumber = cityPrefix + "-" + POINT_BIZ_TYPE + "-" + String.format("%0" + SERIAL_WIDTH + "d", nextSerial); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new ServiceException("点位编号生成繁忙,请稍后重试", I18nKeyConstant.POINT_NUMBER_LOCK_FAIL); } finally { try { if (locked && lock.isHeldByCurrentThread()) { lock.unlock(); } } catch (Exception e) { log.warn("释放点位编号生成锁失败 lockKey={}", lockKey, e); } } XpPoint xpPoint = new XpPoint(); BeanUtils.copyProperties(pointAddDto, xpPoint); xpPoint.setPointNumber(pointNumber); this.save(xpPoint); } @Override public NearestVo nearest(NearestDto dto) { @@ -112,6 +186,57 @@ } private String buildCityPrefix(Long electronicAreaId) { XpElectronicFenceArea area = xpElectronicFenceAreaService.getById(electronicAreaId); if (area == null) { throw new ServiceException("运营区域不存在", I18nKeyConstant.POINT_ELECTRONIC_AREA_NOT_EXIST); } if (!StringUtils.hasText(area.getCityCode())) { throw new ServiceException("城市信息不存在", I18nKeyConstant.POINT_CITY_NOT_EXIST); } XpRegion region = xpRegionMapper.selectOne(new LambdaQueryWrapper<XpRegion>() .eq(XpRegion::getCode, area.getCityCode())); if (region == null || !StringUtils.hasText(region.getName())) { throw new ServiceException("城市信息不存在", I18nKeyConstant.POINT_CITY_NOT_EXIST); } String name = region.getName(); String prefix = name.length() <= 2 ? name : name.substring(0, 2); return getPinyinInitials(prefix).toUpperCase(); } private static String buildPointNumberLockKey(String cityPrefix, String bizType) { return "xp:point:number:" + cityPrefix + ":" + bizType; } private static String getPinyinInitials(String chinese) { StringBuilder pinyinInitials = new StringBuilder(); for (char c : chinese.toCharArray()) { String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c); if (pinyinArray != null && pinyinArray.length > 0) { pinyinInitials.append(pinyinArray[0].charAt(0)); } } return pinyinInitials.toString(); } private static int parseSerialOrThrow(String pointNumber) { String[] parts = pointNumber.split("-"); if (parts.length < 3) { throw new ServiceException("点位编号生成失败(历史编号格式异常)", I18nKeyConstant.POINT_NUMBER_PARSE_FAIL); } String serialPart = parts[parts.length - 1]; if (serialPart.length() != SERIAL_WIDTH) { throw new ServiceException("点位编号生成失败(历史编号格式异常)", I18nKeyConstant.POINT_NUMBER_PARSE_FAIL); } try { return Integer.parseInt(serialPart); } catch (NumberFormatException e) { throw new ServiceException("点位编号生成失败(历史编号格式异常)", I18nKeyConstant.POINT_NUMBER_PARSE_FAIL); } } public boolean getInTime(LocalTime now, Integer effectivePeriod, String effectiveTimePeriod){ xp-robotaxi-map-boot/src/main/resources/i18n/messages.properties
@@ -34,3 +34,8 @@ cruise.plan.vehicle.already.bound=车辆已关联其他巡游计划 cruise.plan.add.fail=新增巡游计划失败 cruise.plan.edit.fail=编辑巡游计划失败 point.electronic.area.id.empty=运营区域ID不能为空 point.electronic.area.not.exist=运营区域不存在 point.city.not.exist=城市信息不存在 point.number.lock.fail=点位编号生成繁忙,请稍后重试 point.number.parse.fail=点位编号生成失败(历史编号格式异常) xp-robotaxi-map-boot/src/main/resources/i18n/messages_en_US.properties
@@ -27,3 +27,8 @@ cruise.plan.vehicle.duplicate=Duplicate vehicle ID cruise.plan.vehicle.already.bound=Vehicle [{0}] has been associated with other cruising plans cruise.plan.add.fail=The new cruise plan failed point.electronic.area.id.empty=Electronic fence area ID cannot be empty point.electronic.area.not.exist=Electronic fence area does not exist point.city.not.exist=City information does not exist point.number.lock.fail=Point number generation is busy, please try again later point.number.parse.fail=Point number generation failed (invalid historical number format) xp-robotaxi-map-boot/src/main/resources/i18n/messages_zh_CN.properties
@@ -27,3 +27,8 @@ cruise.plan.vehicle.duplicate=车辆ID重复 cruise.plan.vehicle.already.bound=车辆[{0}]已关联其他巡游计划 cruise.plan.add.fail=新增巡游计划失败 point.electronic.area.id.empty=运营区域ID不能为空 point.electronic.area.not.exist=运营区域不存在 point.city.not.exist=城市信息不存在 point.number.lock.fail=点位编号生成繁忙,请稍后重试 point.number.parse.fail=点位编号生成失败(历史编号格式异常) xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/SysMenuController.java
@@ -5,10 +5,8 @@ import com.xiaopeng.common.core.web.controller.BaseController; import com.xiaopeng.operation.service.ISysMenuService; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import io.swagger.annotations.ApiParam; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.ArrayList; @@ -46,10 +44,10 @@ * 获取全部菜单树 * @return */ @GetMapping("/api/v1/getMenuTree") @GetMapping("/api/v1/getMenuTree/{type}") @ApiOperation(value = "获取全部菜单树", tags = {"管理后台-权限组设置"}) public R<List<SysMenu>> getMenuTree() { List<SysMenu> menus = menuService.getMenuTree(); public R<List<SysMenu>> getMenuTree(@ApiParam(name = "type", value = "1=管理端 2=运维app") @PathVariable Integer type) { List<SysMenu> menus = menuService.getMenuTree(type); return R.ok(menus); } @@ -59,4 +57,10 @@ return R.ok(menuService.getPermission()); } @GetMapping("/api/v1/getAppPermission") @ApiOperation(value = "获取获取当前登录用户权限列表", tags = {"运维APP-个人中心"}) public R<List<SysMenu>> getAppPermission(){ return R.ok(menuService.getAppPermission()); } } xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/ISysMenuService.java
@@ -27,11 +27,17 @@ *获取全部菜单树 * @return */ List<SysMenu> getMenuTree(); List<SysMenu> getMenuTree(Integer type); /** *获取获取当前登录用户菜单列表 * @return */ List<SysMenu> getPermission(); /** * 获取运维人员权限列表 * @return */ List<SysMenu> getAppPermission(); } xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/SysMenuServiceImpl.java
@@ -4,8 +4,10 @@ import com.xiaopeng.api.feignClient.account.SysGroupMenuClient; import com.xiaopeng.api.feignClient.account.SysRoleClient; import com.xiaopeng.api.feignClient.account.SysUserRoleClient; import com.xiaopeng.api.feignClient.account.XpOperationsRoleClient; import com.xiaopeng.api.model.account.SysRole; import com.xiaopeng.api.model.account.SysUserRole; import com.xiaopeng.api.model.account.XpOperationsRole; import com.xiaopeng.api.model.operation.SysMenu; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.security.service.TokenService; @@ -42,6 +44,9 @@ @Autowired private SysGroupMenuClient sysGroupMenuClient; @Autowired private XpOperationsRoleClient xpOperationsRoleClient; /** * 根据菜单ID列表查询菜单列表 * @@ -59,8 +64,8 @@ } @Override public List<SysMenu> getMenuTree() { List<SysMenu> all = list(); public List<SysMenu> getMenuTree(Integer type) { List<SysMenu> all = lambdaQuery().eq(SysMenu::getGroupType, type).list(); if (all == null || all.isEmpty()) { return Collections.emptyList(); } @@ -184,4 +189,139 @@ return Collections.emptyList(); } } @Override public List<SysMenu> getAppPermission() { // 1. 获取当前登录用户ID Long operationUserId = tokenService.getLoginOperationUser().getId(); if (operationUserId == null) { log.warn("获取当前登录用户ID失败"); return Collections.emptyList(); } // 2. 查询运维APP菜单(group_type=2) List<SysMenu> appMenus = this.lambdaQuery() .eq(SysMenu::getGroupType, 2) .list(); if (appMenus == null || appMenus.isEmpty()) { return Collections.emptyList(); } // 3. 查询运维人员关联的角色(一个用户可多个角色) Set<Long> roleIds = new HashSet<>(); try { R<List<XpOperationsRole>> rolesResult = xpOperationsRoleClient.listByOperationUserId(operationUserId); if (rolesResult != null && rolesResult.getData() != null) { for (XpOperationsRole r : rolesResult.getData()) { if (r != null && r.getRoleId() != null) { roleIds.add(r.getRoleId()); } } } } catch (Exception e) { log.error("查询运维人员角色关联异常, operationUserId={}", operationUserId, e); } // 4. 通过 role -> groupId -> group_menu 获取允许的菜单ID集合 Set<Long> allowedMenuIds = new HashSet<>(); if (!roleIds.isEmpty()) { Set<Long> groupIds = new HashSet<>(); for (Long roleId : roleIds) { try { R<SysRole> roleResult = sysRoleClient.getRoleById(roleId); if (roleResult != null && roleResult.getData() != null && roleResult.getData().getGroupId() != null) { groupIds.add(roleResult.getData().getGroupId()); } } catch (Exception e) { log.error("查询角色信息异常, roleId={}", roleId, e); } } for (Long groupId : groupIds) { try { R<List<Long>> menuIdsResult = sysGroupMenuClient.selectMenuIdsByGroupId(groupId); if (menuIdsResult != null && menuIdsResult.getData() != null) { allowedMenuIds.addAll(menuIdsResult.getData()); } } catch (Exception e) { log.error("查询权限组菜单ID异常, groupId={}", groupId, e); } } } // 5. 仅保留运维APP的菜单ID,避免无关ID影响 visible Set<Long> appMenuIds = appMenus.stream() .filter(Objects::nonNull) .map(SysMenu::getId) .filter(Objects::nonNull) .collect(Collectors.toSet()); allowedMenuIds.retainAll(appMenuIds); // 6. 组装树形结构,并用 visible 体现权限可见性(0可见 1不可见) return buildAppMenuTreeWithVisible(appMenus, allowedMenuIds); } private List<SysMenu> buildAppMenuTreeWithVisible(List<SysMenu> menus, Set<Long> allowedMenuIds) { Map<Long, SysMenu> idMap = new HashMap<>(); for (SysMenu m : menus) { if (m != null && m.getId() != null) { idMap.put(m.getId(), m); } } Map<Long, List<SysMenu>> childrenMap = menus.stream() .filter(Objects::nonNull) .filter(m -> m.getId() != null) .collect(Collectors.groupingBy(m -> { Long pid = m.getParentId(); return pid == null ? 0L : pid; })); // 根节点:parentId 为 0/null,或父节点不在当前 group_type=2 集合内 List<SysMenu> roots = menus.stream() .filter(Objects::nonNull) .filter(m -> m.getId() != null) .filter(m -> { Long pid = m.getParentId(); if (pid == null || pid == 0L) { return true; } return !idMap.containsKey(pid); }) .sorted(Comparator.comparing(SysMenu::getOrderNum, Comparator.nullsLast(Integer::compareTo))) .collect(Collectors.toList()); for (SysMenu root : roots) { fillChildrenAndVisible(root, childrenMap, allowedMenuIds, new HashSet<>()); } return roots; } private boolean fillChildrenAndVisible(SysMenu node, Map<Long, List<SysMenu>> childrenMap, Set<Long> allowedMenuIds, Set<Long> path) { Long id = node.getId(); if (id != null && !path.add(id)) { boolean visible = allowedMenuIds.contains(id); node.setChildren(Collections.emptyList()); node.setVisible(visible ? "0" : "1"); return visible; } List<SysMenu> children = childrenMap.getOrDefault(id == null ? 0L : id, Collections.emptyList()).stream() .filter(Objects::nonNull) .sorted(Comparator.comparing(SysMenu::getOrderNum, Comparator.nullsLast(Integer::compareTo))) .collect(Collectors.toList()); boolean anyChildVisible = false; for (SysMenu child : children) { anyChildVisible |= fillChildrenAndVisible(child, childrenMap, allowedMenuIds, path); } node.setChildren(children); boolean selfVisible = id != null && allowedMenuIds.contains(id); boolean visible = selfVisible || anyChildVisible; node.setVisible(visible ? "0" : "1"); if (id != null) { path.remove(id); } return visible; } }