“mitao”
2026-03-11 6eed8cac891460d48b84e16dade92ab110a1b177
bugfix
17个文件已修改
438 ■■■■■ 已修改文件
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/controller/XpOperationsRoleController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/account/XpOperationUserClientFallbackFactory.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/account/XpOperationUserClient.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpTaskServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpElectronicFenceManagementController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/constant/I18nKeyConstant.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpInvitationCodeController.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpWhitelistController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpInvitationCodeServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpSystemSettingServiceImpl.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpWhitelistServiceImpl.java 193 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/resources/i18n/messages_en_US.properties 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/resources/i18n/messages_zh_CN.properties 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/constant/I18nKeyConstant.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/service/impl/XpVehicleServiceImpl.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-vehicle-convoy-boot/src/main/resources/i18n/messages_en_US.properties 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-vehicle-convoy-boot/src/main/resources/i18n/messages_zh_CN.properties 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-account-boot/src/main/java/com/xiaopeng/account/controller/XpOperationsRoleController.java
@@ -1,14 +1,12 @@
package com.xiaopeng.account.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xiaopeng.account.service.IXpOperationsRoleService;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@@ -53,5 +51,15 @@
                .eq("operations_user_id", operationUserId));
        return R.ok(list);
    }
    /**
     * 内部调用
     * 查询角色关联的运维人员
     * @param roleIds
     * @return
     */
    @PostMapping("/inner/xp-operations-role/getXpOperationsRoleByRoleIds")
    public R<List<XpOperationsRole>> getXpOperationsRoleByRoleIds(@RequestBody List<Long> roleIds) {
        List<XpOperationsRole> list = xpOperationsRoleService.list(new LambdaQueryWrapper<XpOperationsRole>().in(XpOperationsRole::getRoleId, roleIds));
        return R.ok(list);
    }
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/factory/account/XpOperationUserClientFallbackFactory.java
@@ -64,6 +64,11 @@
                return R.ok(new ArrayList<>());
            }
            @Override
            public R<List<XpOperationsRole>> getXpOperationsRoleByRoleIds(List<Long> roleIds) {
                log.error("根据角色ID列表获取运维人员ID列表失败,原因:{}", cause.getMessage());
                return R.fail("根据角色ID列表获取运维人员ID列表失败:" + cause.getMessage());
            }
        };
    }
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/feignClient/account/XpOperationUserClient.java
@@ -78,4 +78,12 @@
     */
    @GetMapping("/inner/xpOperationsUser/listAllEnabled")
    R<List<XpOperationsUser>> listAllEnabled();
    /**
     * 查询角色关联的运维人员
     * @param roleIds
     * @return
     */
    @PostMapping("/inner/xp-operations-role/getXpOperationsRoleByRoleIds")
    R<List<XpOperationsRole>> getXpOperationsRoleByRoleIds(@RequestBody List<Long> roleIds);
}
xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpTaskServiceImpl.java
@@ -229,13 +229,14 @@
        xpTask.setStatus(0);
        xpTask.setCreateBy(xpOperationsUser.getNickName());
        XpTaskRule taskRule = xpTaskRuleService.lambdaQuery().last("limit 1").one();
        XpTaskRuleRole taskRuleRole = xpTaskRuleRoleService.lambdaQuery().eq(XpTaskRuleRole::getRuleType, 9).last("limit 1").one();
        if (Objects.isNull(taskRuleRole) || Objects.isNull(taskRule)) {
        List<XpTaskRuleRole> list = xpTaskRuleRoleService.lambdaQuery().eq(XpTaskRuleRole::getRuleType, 9).list();
        if (CollUtil.isEmpty(list) || Objects.isNull(taskRule)) {
            throw new ServiceException("未配置车辆事故任务分配规则");
        }
        R<List<XpOperationsRole>> xpOperationsRoleResult = xpOperationUserClient.getXpOperationsRoleByRoleId(taskRuleRole.getRoleId());
        List<Long> roleIds = list.stream().map(XpTaskRuleRole::getRoleId).collect(Collectors.toList());
        R<List<XpOperationsRole>> xpOperationsRoleResult = xpOperationUserClient.getXpOperationsRoleByRoleIds(roleIds);
        if (CollUtil.isNotEmpty(xpOperationsRoleResult.getData())) {
            List<Long> userIds = xpOperationsRoleResult.getData().stream().map(XpOperationsRole::getOperationsUserId).collect(Collectors.toList());
            List<Long> userIds = xpOperationsRoleResult.getData().stream().map(XpOperationsRole::getOperationsUserId).distinct().collect(Collectors.toList());
            xpTask.setHandleUserId(StringUtils.join(userIds, ","));
        }
        // 根据事故处理限时配置设置任务截止时间
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpElectronicFenceManagementController.java
@@ -2,6 +2,7 @@
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaopeng.api.dto.operation.ElectronicFenceAddDto;
import com.xiaopeng.api.dto.operation.ElectronicFenceAreaUpdateStatusDto;
import com.xiaopeng.api.dto.operation.ElectronicFenceUpdateDto;
@@ -26,6 +27,8 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType;
import org.apache.shardingsphere.transaction.core.TransactionType;
import org.locationtech.jts.geom.Coordinate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -34,6 +37,9 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static com.xiaopeng.map.service.serviceimpl.XpElectronicFenceServiceImpl.getPinyinInitials;
/**
 * <p>
@@ -58,6 +64,8 @@
    @Resource
    private XpRegionService regionService;
    @Autowired
    private XpRegionService xpRegionService;
    /**
     * 查询省市管理
     */
@@ -166,9 +174,21 @@
        electronicFence.setElectronicName("副本_"+electronicFence.getElectronicName());
        electronicFence.setId(null);
        electronicFence.setStatus(0);
        xpElectronicFenceService.save(electronicFence);
        List<XpElectronicFenceArea> electronicFenceAreaList = xpElectronicFenceAreaService.lambdaQuery().eq(XpElectronicFenceArea::getElectronicId, id)
                .list();
        //根据经纬度获取当前城市code
        // 生成围栏id 规则:城市首字母-类型简写-4位自增流水号(如 SZ-OP-001),支持复制;数据停用或删除后,流水号不回收
        XpRegion xpRegion = xpRegionService.getOne(new LambdaQueryWrapper<XpRegion>()
                .eq(XpRegion::getCode, electronicFence.getCityCode()));
        String name = xpRegion.getName();
        // 根据城市中文名,截取前两位的首字母并且大写
        // 截取前两个字符
        String prefix = name.substring(0, 2);
        // 获取拼音首字母并大写
        String result = getPinyinInitials(prefix).toUpperCase();
        xpElectronicFenceService.save(electronicFence);
        electronicFence.setElectronicNumber(result + "-OP-" + String.format("%04d", electronicFence.getId()));
        xpElectronicFenceService.updateById(electronicFence);
        for (XpElectronicFenceArea area : electronicFenceAreaList) {
            area.setId(null);
            area.setElectronicId(electronicFence.getId());
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/constant/I18nKeyConstant.java
@@ -97,4 +97,7 @@
    String INVITATION_IMPORT_CODE_CONTAINS_CHINESE = "invitation.import.code.contains.chinese";
    String INVITATION_IMPORT_CODE_DUPLICATE = "invitation.import.code.duplicate";
    String INVITATION_IMPORT_CODE_EXISTS = "invitation.import.code.exists";
    // 邀请码数量上限
    String INVITATION_LIMIT_EXCEEDED = "invitation.limit.exceeded";
}
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpInvitationCodeController.java
@@ -6,6 +6,7 @@
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.TemplateExportParams;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.PhoneUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
@@ -67,6 +68,7 @@
    private MessageSource messageSource;
    private static final Pattern CHINESE_PATTERN = Pattern.compile("\\p{IsHan}");
    private static final long INVITATION_CODE_LIMIT = 500L;
    @ApiOperation(value = "邀请码管理分页列表",response = XpInvitationCode.class)
    @PostMapping("/api/v1/invitation/pageList")
@@ -82,6 +84,18 @@
    @ApiOperation(value = "生成邀请码")
    @Transactional
    public R add(@RequestBody InvitationAddDto dto){
        long existing = xpInvitationCodeService.count();
        long incoming = Arrays.stream(Optional.ofNullable(dto.getPhones()).orElse("").split(","))
                .map(String::trim)
                .filter(StringUtils::hasLength)
                .count();
        if (existing >= INVITATION_CODE_LIMIT || existing + incoming > INVITATION_CODE_LIMIT) {
            String msg = messageSource.getMessage(I18nKeyConstant.INVITATION_LIMIT_EXCEEDED,
                    new Object[]{INVITATION_CODE_LIMIT, existing, incoming},
                    I18nKeyConstant.INVITATION_LIMIT_EXCEEDED,
                    LocaleContextHolder.getLocale());
            return R.fail(msg);
        }
        return xpInvitationCodeService.add(dto.getPhones());
    }
    /**
@@ -158,6 +172,16 @@
        }
        if (CollectionUtils.isEmpty(invitationCodeImportExcels)) {
            return R.fail(messageSource.getMessage(I18nKeyConstant.INVITATION_IMPORT_DATA_EMPTY, null, I18nKeyConstant.INVITATION_IMPORT_DATA_EMPTY, LocaleContextHolder.getLocale()));
        }
        long existing = xpInvitationCodeService.count();
        long incoming = invitationCodeImportExcels.size();
        if (existing >= INVITATION_CODE_LIMIT || existing + incoming > INVITATION_CODE_LIMIT) {
            String msg = messageSource.getMessage(I18nKeyConstant.INVITATION_LIMIT_EXCEEDED,
                    new Object[]{INVITATION_CODE_LIMIT, existing, incoming},
                    I18nKeyConstant.INVITATION_LIMIT_EXCEEDED,
                    LocaleContextHolder.getLocale());
            return R.fail(msg);
        }
        // 批量校验:账号必须为手机号;邀请码不能为空且不能包含中文;同时校验文件内重复/数据库已存在
@@ -320,5 +344,16 @@
        xpInvitationCodeService.removeBatchByIds(longs);
        return R.ok();
    }
    @ApiOperation(value = "校验生成邀请码的手机号是否合法")
    @PostMapping("/api/v1/invitation/validateCode")
    public R<Boolean> validateCode(@RequestParam String phone){
        if (!PhoneUtil.isMobile(phone)){
            return R.ok(false);
        }
        List<XpInvitationCode> existsList = xpInvitationCodeService.lambdaQuery()
                .in(XpInvitationCode::getBindAccount, phone)
                .list();
        return R.ok(CollUtil.isEmpty(existsList));
    }
}
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpWhitelistController.java
@@ -242,7 +242,7 @@
                recordFail(reasonCountMap, reasonSampleMap, "账号为空", "第" + excelRowNumber + "行", sampleLimitPerReason);
                continue;
            }
            if (!PhoneUtil.isPhone(account)) {
            if (!PhoneUtil.isMobile(account)) {
                recordFail(reasonCountMap, reasonSampleMap, "账号格式不正确", account, sampleLimitPerReason);
                continue;
            }
@@ -406,9 +406,10 @@
    public void exportExcel(@RequestBody WhitelistPageListQuery query) {
        List<XpWhitelist> exportList = xpWhitelistService.exportList(query);
        List<XpWhitelistExportExcel> xpWhitelistExportExcels = new ArrayList<>();
        long sequence = 1L;
        for (XpWhitelist xpWhitelist : exportList) {
            XpWhitelistExportExcel xpWhitelistExportExcel = new XpWhitelistExportExcel();
            xpWhitelistExportExcel.setId(xpWhitelist.getId());
            xpWhitelistExportExcel.setId(sequence++);
            xpWhitelistExportExcel.setAccount(xpWhitelist.getAccount());
            xpWhitelistExportExcel.setNickName(xpWhitelist.getNickName());
            xpWhitelistExportExcel.setStatus(xpWhitelist.getStatus());
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpInvitationCodeServiceImpl.java
@@ -5,6 +5,7 @@
import com.xiaopeng.api.model.operation.XpWhitelist;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.web.page.PageInfo;
import com.xiaopeng.operation.constant.I18nKeyConstant;
import com.xiaopeng.operation.mapper.XpInvitationCodeMapper;
import com.xiaopeng.operation.mapper.XpWhitelistMapper;
import com.xiaopeng.operation.query.InvitationPageListQuery;
@@ -31,6 +32,7 @@
    @Autowired
    private XpWhitelistMapper xpWhitelistMapper;
    private static final long INVITATION_CODE_LIMIT = 500L;
    @Override
    public PageInfo<XpInvitationCode> pageList(InvitationPageListQuery query) {
        PageInfo<XpInvitationCode> pageInfo = new PageInfo<>(query.getPageNum(),query.getPageSize());
@@ -48,6 +50,16 @@
    @Override
    public R add(String phones) {
        long existing = this.count();
        long incoming = Arrays.stream(Optional.ofNullable(phones).orElse("").split(","))
                .map(String::trim)
                .filter(StringUtils::hasLength)
                .count();
        if (existing >= INVITATION_CODE_LIMIT || existing + incoming > INVITATION_CODE_LIMIT) {
            return R.fail(String.format("系统最多只能有%d个邀请码,当前已有%d个,本次新增%d个,无法生成", INVITATION_CODE_LIMIT, existing, incoming),
                    I18nKeyConstant.INVITATION_LIMIT_EXCEEDED);
        }
        Map<String, Long> codeMap = this.list().stream()
                .collect(Collectors.toMap(XpInvitationCode::getInvitationCode, XpInvitationCode::getId));
        if (StringUtils.hasLength(phones)){
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpSystemSettingServiceImpl.java
@@ -66,6 +66,7 @@
        HotFixInfoVo hotFixInfoVo = new HotFixInfoVo();
        hotFixInfoVo.setHotfixStatus(xpSystemSetting.getHotfixStatus() == null ? 0 : xpSystemSetting.getHotfixStatus());
        hotFixInfoVo.setHotfixStartTime(xpSystemSetting.getHotfixStartTime());
        hotFixInfoVo.setHotfixEndTime(xpSystemSetting.getHotfixEndTime());
        redisService.setCacheObject("HOT_FIX_INFO", hotFixInfoVo);
    }
}
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpWhitelistServiceImpl.java
@@ -8,7 +8,6 @@
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.exception.ServiceException;
import com.xiaopeng.common.core.utils.ip.IpUtils;
import com.xiaopeng.common.core.web.domain.AjaxResult;
import com.xiaopeng.common.core.web.page.PageInfo;
import com.xiaopeng.operation.constant.I18nKeyConstant;
import com.xiaopeng.operation.dto.WhitelistAddDto;
@@ -23,8 +22,13 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -56,41 +60,164 @@
    @Override
    public R addWhitelist(WhitelistAddDto dto, SysUser sysUser) {
        XpWhitelist old = this.lambdaQuery().eq(XpWhitelist::getAccount, dto.getAccount()).last("limit 1").one();
        if (old != null) {
            return R.fail("该账号已存在");
        List<String> accounts = Arrays.stream(dto.getAccount().split(","))
                .map(String::trim)
                .filter(StringUtils::hasLength)
                .collect(Collectors.toList());
        if (CollUtil.isEmpty(accounts)) {
            return R.fail("所有帐号添加失败,请检查帐号格式或唯一性");
        }
        int sampleLimitPerReason = 20;
        int totalCount = accounts.size();
        Map<String, Long> reasonCountMap = new HashMap<>();
        Map<String, LinkedHashSet<String>> reasonSampleMap = new HashMap<>();
        Set<String> seenAccounts = new HashSet<>();
        List<String> candidates = new ArrayList<>();
        for (String account : accounts) {
            if (!PhoneUtil.isMobile(account)) {
                recordFail(reasonCountMap, reasonSampleMap, "帐号格式不正确", account, sampleLimitPerReason);
                continue;
            }
            if (!seenAccounts.add(account)) {
                recordFail(reasonCountMap, reasonSampleMap, "文件内重复", account, sampleLimitPerReason);
                continue;
            }
            candidates.add(account);
        }
        Set<String> existsAccounts = new HashSet<>();
        if (CollUtil.isNotEmpty(candidates)) {
            existsAccounts = this.lambdaQuery()
                    .select(XpWhitelist::getAccount)
                    .in(XpWhitelist::getAccount, candidates)
                    .list()
                    .stream()
                    .map(XpWhitelist::getAccount)
                    .filter(StringUtils::hasLength)
                    .collect(Collectors.toSet());
        }
        List<XpWhitelist> addList = new ArrayList<>();
        List<String> accounts = Arrays.asList(dto.getAccount().split(","));
        if (CollUtil.isNotEmpty(accounts)) {
            String notValidPhoneNumber = accounts.stream().filter(item -> !PhoneUtil.isPhone(item)).collect(Collectors.joining(","));
            if (org.apache.commons.lang3.StringUtils.isNotBlank(notValidPhoneNumber)) {
                throw new ServiceException("以下帐号格式不正确:" + notValidPhoneNumber, I18nKeyConstant.ACCOUNT_FORMAT_ERROR, new Object[]{notValidPhoneNumber});
        for (String account : candidates) {
            if (existsAccounts.contains(account)) {
                recordFail(reasonCountMap, reasonSampleMap, "帐号已存在", account, sampleLimitPerReason);
                continue;
            }
            StringBuilder operationContent = new StringBuilder("新增账号:");
            StringBuilder accountBuilder = new StringBuilder();
            for (String account : accounts) {
                XpWhitelist xpWhitelist = new XpWhitelist();
                xpWhitelist.setAccount(account);
                xpWhitelist.setStatus(true);
                xpWhitelist.setIsPriorityDispatch(dto.getIsPriorityDispatch());
                xpWhitelist.setWhitelistSource(1);
                xpWhitelist.setCreateBy(sysUser.getCreateBy());
                addList.add(xpWhitelist);
                accountBuilder.append("\"").append(account).append("\"").append(",");
            }
            XpWhitelistOperlog xpWhitelistOperlog = new XpWhitelistOperlog();
            xpWhitelistOperlog.setOperationName(sysUser.getNickName());
            xpWhitelistOperlog.setOperationAccount(sysUser.getPhoneNumber());
            xpWhitelistOperlog.setOperationType(1);
            xpWhitelistOperlog.setOperationIp(IpUtils.getIpAddr());
            xpWhitelistOperlog.setOperationAccountId(sysUser.getId());
            StringBuilder content = operationContent.append(accountBuilder.substring(0, accountBuilder.length() - 1));
            xpWhitelistOperlog.setOperationContent(content.toString());
            xpWhitelistOperlogMapper.insert(xpWhitelistOperlog);
            this.saveBatch(addList);
            XpWhitelist xpWhitelist = new XpWhitelist();
            xpWhitelist.setAccount(account);
            xpWhitelist.setStatus(true);
            xpWhitelist.setIsPriorityDispatch(dto.getIsPriorityDispatch());
            xpWhitelist.setWhitelistSource(1);
            xpWhitelist.setCreateBy(sysUser.getNickName());
            addList.add(xpWhitelist);
        }
        return R.ok();
        int successCount = addList.size();
        int failCount = totalCount - successCount;
        String message;
        if (failCount == 0) {
            message = "成功添加" + successCount + "条白名单记录";
        } else if (successCount == 0) {
            message = "所有帐号添加失败,请检查帐号格式或唯一性";
        } else {
            message = "成功加" + successCount + "条," + failCount + "条添加失败,失败原因:" + buildReasonDetail(reasonCountMap, reasonSampleMap);
        }
        if (successCount == 0) {
            return R.fail(message);
        }
        this.saveBatch(addList);
        StringBuilder operationContent = new StringBuilder("新增账号:");
        StringBuilder accountBuilder = new StringBuilder();
        for (XpWhitelist item : addList) {
            accountBuilder.append("\"").append(item.getAccount()).append("\"").append(",");
        }
        if (accountBuilder.length() > 0) {
            operationContent.append(accountBuilder.substring(0, accountBuilder.length() - 1));
        }
        if (failCount > 0) {
            operationContent.append(";失败").append(failCount).append("条");
            operationContent.append(";原因").append(buildReasonSummary(reasonCountMap));
        }
        XpWhitelistOperlog xpWhitelistOperlog = new XpWhitelistOperlog();
        xpWhitelistOperlog.setOperationName(sysUser.getNickName());
        xpWhitelistOperlog.setOperationAccount(sysUser.getPhoneNumber());
        xpWhitelistOperlog.setOperationType(1);
        xpWhitelistOperlog.setOperationIp(IpUtils.getIpAddr());
        xpWhitelistOperlog.setOperationAccountId(sysUser.getId());
        xpWhitelistOperlog.setOperationContent(truncate(operationContent.toString(), 200));
        xpWhitelistOperlogMapper.insert(xpWhitelistOperlog);
        return R.ok(null, message);
    }
    private static void recordFail(Map<String, Long> reasonCountMap,
                                   Map<String, LinkedHashSet<String>> reasonSampleMap,
                                   String reason,
                                   String sample,
                                   int sampleLimitPerReason) {
        reasonCountMap.merge(reason, 1L, Long::sum);
        if (!StringUtils.hasLength(sample) || sampleLimitPerReason <= 0) {
            return;
        }
        LinkedHashSet<String> samples = reasonSampleMap.computeIfAbsent(reason, k -> new LinkedHashSet<>());
        if (samples.size() >= sampleLimitPerReason) {
            return;
        }
        samples.add(sample);
    }
    private static String buildReasonSummary(Map<String, Long> reasonCountMap) {
        if (reasonCountMap == null || reasonCountMap.isEmpty()) {
            return "请检查帐号格式或唯一性";
        }
        return reasonCountMap.entrySet().stream()
                .sorted(Comparator.<Map.Entry<String, Long>>comparingLong(e -> e.getValue() == null ? 0L : e.getValue())
                        .reversed()
                        .thenComparing(Map.Entry::getKey))
                .map(e -> e.getKey() + "(" + (e.getValue() == null ? 0L : e.getValue()) + ")")
                .collect(Collectors.joining(","));
    }
    private static String buildReasonDetail(Map<String, Long> reasonCountMap,
                                           Map<String, LinkedHashSet<String>> reasonSampleMap) {
        if (reasonCountMap == null || reasonCountMap.isEmpty()) {
            return "请检查帐号格式或唯一性";
        }
        Map<String, LinkedHashSet<String>> safeSamples = reasonSampleMap == null ? new HashMap<>() : reasonSampleMap;
        return reasonCountMap.entrySet().stream()
                .sorted(Comparator.<Map.Entry<String, Long>>comparingLong(e -> e.getValue() == null ? 0L : e.getValue())
                        .reversed()
                        .thenComparing(Map.Entry::getKey))
                .map(entry -> {
                    String reason = entry.getKey();
                    long count = entry.getValue() == null ? 0L : entry.getValue();
                    LinkedHashSet<String> samples = safeSamples.getOrDefault(reason, new LinkedHashSet<>());
                    if (samples.isEmpty()) {
                        return reason + "(" + count + ")";
                    }
                    long remaining = count - samples.size();
                    String suffix = remaining > 0 ? "等" + remaining + "个" : "";
                    String suffixPart = StringUtils.hasLength(suffix) ? ("," + suffix) : "";
                    return reason + "(" + count + ")[" + String.join(",", samples) + suffixPart + "]";
                })
                .collect(Collectors.joining(";"));
    }
    private static String truncate(String value, int maxLength) {
        if (value == null || maxLength <= 0) {
            return value;
        }
        if (value.length() <= maxLength) {
            return value;
        }
        return value.substring(0, maxLength);
    }
    @Override
xp-robotaxi-operation-boot/src/main/resources/i18n/messages_en_US.properties
@@ -36,3 +36,4 @@
invitation.import.code.contains.chinese=Row {0}: invitation code contains Chinese characters: {1}
invitation.import.code.duplicate=Duplicate invitation codes in file: {0}
invitation.import.code.exists=The following invitation codes already exist: {0}
invitation.limit.exceeded=The system can hold up to {0} invitation codes. Current: {1}, incoming: {2}. Generation is not allowed.
xp-robotaxi-operation-boot/src/main/resources/i18n/messages_zh_CN.properties
@@ -36,3 +36,4 @@
invitation.import.code.contains.chinese=第{0}行邀请码包含中文:{1}
invitation.import.code.duplicate=导入文件中邀请码重复:{0}
invitation.import.code.exists=以下邀请码已存在:{0}
invitation.limit.exceeded=系统最多只能有{0}个邀请码,当前已有{1}个,本次新增{2}个,无法生成
xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/constant/I18nKeyConstant.java
@@ -40,4 +40,14 @@
    String VEHICLE_TEMPERATURE_DOWN = "vehicle.temperature.down";
    // ========== 车辆新增/编辑校验 ==========
    String VEHICLE_CODE_INVALID = "vehicle.code.invalid";
    String VEHICLE_CODE_EXISTS = "vehicle.code.exists";
    String VEHICLE_VIN_INVALID = "vehicle.vin.invalid";
    String VEHICLE_VIN_EXISTS = "vehicle.vin.exists";
    String VEHICLE_PLATE_INVALID = "vehicle.plate.invalid";
    String VEHICLE_PLATE_EXISTS = "vehicle.plate.exists";
    String VEHICLE_COLOR_INVALID = "vehicle.color.invalid";
    String VEHICLE_ELECTRONIC_NOT_EXISTS = "vehicle.electronic.not.exists";
}
xp-robotaxi-vehicle-convoy-boot/src/main/java/com/xiaopeng/vehicleConvoy/service/impl/XpVehicleServiceImpl.java
@@ -2,6 +2,7 @@
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Validator;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -58,6 +59,7 @@
import java.time.LocalTime;
import java.util.*;
import java.util.stream.Collectors;
import java.util.regex.Pattern;
/**
 * <p>
@@ -70,6 +72,9 @@
@Slf4j
@Service
public class XpVehicleServiceImpl extends ServiceImpl<XpVehicleMapper, XpVehicle> implements IXpVehicleService {
    private static final Pattern VEHICLE_CODE_PATTERN = Pattern.compile("^[A-Za-z0-9_]{5}$");
    private static final Pattern VEHICLE_VIN_PATTERN = Pattern.compile("^[A-Z0-9]{17}$");
    @Autowired
    private VehicleControlUtil vehicleControlUtil;
@@ -309,6 +314,18 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void addVehicle(VehicleAddDTO dto) {
        String normalizedVehicleCode = trimToNull(dto.getVehicleCode());
        String normalizedVin = normalizeVin(dto.getVehicleVin());
        String normalizedPlateNumber = trimToNull(dto.getPlateNumber());
        String normalizedVehicleColor = trimToNull(dto.getVehicleColor());
        validateAndNormalizeIdentityFields(normalizedVehicleCode, normalizedVin, normalizedPlateNumber, normalizedVehicleColor, null);
        validateElectronicFenceExists(dto.getElectronicId());
        dto.setVehicleCode(normalizedVehicleCode);
        dto.setVehicleVin(normalizedVin);
        dto.setPlateNumber(normalizedPlateNumber);
        dto.setVehicleColor(normalizedVehicleColor);
        // DTO转实体
        XpVehicle xpVehicle = new XpVehicle();
        BeanUtil.copyProperties(dto, xpVehicle);
@@ -329,6 +346,18 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void editVehicle(VehicleEditDTO dto) {
        String normalizedVehicleCode = trimToNull(dto.getVehicleCode());
        String normalizedVin = normalizeVin(dto.getVehicleVin());
        String normalizedPlateNumber = trimToNull(dto.getPlateNumber());
        String normalizedVehicleColor = trimToNull(dto.getVehicleColor());
        validateAndNormalizeIdentityFields(normalizedVehicleCode, normalizedVin, normalizedPlateNumber, normalizedVehicleColor, dto.getId());
        validateElectronicFenceExists(dto.getElectronicId());
        dto.setVehicleCode(normalizedVehicleCode);
        dto.setVehicleVin(normalizedVin);
        dto.setPlateNumber(normalizedPlateNumber);
        dto.setVehicleColor(normalizedVehicleColor);
        // DTO转实体
        XpVehicle xpVehicle = new XpVehicle();
        BeanUtil.copyProperties(dto, xpVehicle);
@@ -339,6 +368,74 @@
        this.updateById(xpVehicle);
    }
    private void validateAndNormalizeIdentityFields(String vehicleCode, String vehicleVin, String plateNumber, String vehicleColor, Long excludeId) {
        if (StringUtils.isBlank(vehicleCode) || !VEHICLE_CODE_PATTERN.matcher(vehicleCode).matches()) {
            throw new ServiceException("车辆编码格式不正确", I18nKeyConstant.VEHICLE_CODE_INVALID, new Object[]{vehicleCode});
        }
        if (StringUtils.isBlank(vehicleVin) || !VEHICLE_VIN_PATTERN.matcher(vehicleVin).matches()) {
            throw new ServiceException("车辆VIN码格式不正确", I18nKeyConstant.VEHICLE_VIN_INVALID, new Object[]{vehicleVin});
        }
        if (StringUtils.isBlank(plateNumber) || !Validator.isPlateNumber(plateNumber)) {
            throw new ServiceException("车牌号格式不正确", I18nKeyConstant.VEHICLE_PLATE_INVALID, new Object[]{plateNumber});
        }
        if (StringUtils.isNotBlank(vehicleColor) && vehicleColor.length() > 2) {
            throw new ServiceException("车辆颜色长度不能超过2", I18nKeyConstant.VEHICLE_COLOR_INVALID, new Object[]{vehicleColor});
        }
        boolean vehicleCodeExists = lambdaQuery()
                .eq(XpVehicle::getVehicleCode, vehicleCode)
                .ne(excludeId != null, XpVehicle::getId, excludeId)
                .last("limit 1")
                .one() != null;
        if (vehicleCodeExists) {
            throw new ServiceException("车辆编码已存在", I18nKeyConstant.VEHICLE_CODE_EXISTS, new Object[]{vehicleCode});
        }
        boolean vehicleVinExists = lambdaQuery()
                .eq(XpVehicle::getVehicleVin, vehicleVin)
                .ne(excludeId != null, XpVehicle::getId, excludeId)
                .last("limit 1")
                .one() != null;
        if (vehicleVinExists) {
            throw new ServiceException("车辆VIN码已存在", I18nKeyConstant.VEHICLE_VIN_EXISTS, new Object[]{vehicleVin});
        }
        boolean plateNumberExists = lambdaQuery()
                .eq(XpVehicle::getPlateNumber, plateNumber)
                .ne(excludeId != null, XpVehicle::getId, excludeId)
                .last("limit 1")
                .one() != null;
        if (plateNumberExists) {
            throw new ServiceException("车牌号已存在", I18nKeyConstant.VEHICLE_PLATE_EXISTS, new Object[]{plateNumber});
        }
    }
    private void validateElectronicFenceExists(Long electronicId) {
        if (electronicId == null) {
            return;
        }
        R<List<XpElectronicFence>> result = xpElectronicClient.listByIds(Collections.singleton(electronicId));
        boolean exists = result != null
                && result.getData() != null
                && result.getData().stream().anyMatch(fence -> fence != null && electronicId.equals(fence.getId()));
        if (!exists) {
            throw new ServiceException("运营区域不存在", I18nKeyConstant.VEHICLE_ELECTRONIC_NOT_EXISTS, new Object[]{electronicId});
        }
    }
    private static String normalizeVin(String vehicleVin) {
        String trimmed = trimToNull(vehicleVin);
        return trimmed == null ? null : trimmed.toUpperCase(Locale.ROOT);
    }
    private static String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void improveInfo(VehicleImproveInfoDTO dto) {
xp-robotaxi-vehicle-convoy-boot/src/main/resources/i18n/messages_en_US.properties
@@ -25,4 +25,12 @@
vehicle.not.p.gear=The trunk cannot be opened when the vehicle is not in P gear
vehicle.temperature.up=The highest temperature is 28°C
vehicle.temperature.down=The lowest temperature is 22°C
vehicle.code.invalid=Invalid vehicle code (5 chars, letters/numbers/underscore only): {0}
vehicle.code.exists=Vehicle code already exists: {0}
vehicle.vin.invalid=Invalid VIN (17 chars, uppercase letters and digits only): {0}
vehicle.vin.exists=VIN already exists: {0}
vehicle.plate.invalid=Invalid plate number: {0}
vehicle.plate.exists=Plate number already exists: {0}
vehicle.color.invalid=Vehicle color length must be <= 2: {0}
vehicle.electronic.not.exists=Selected operating area does not exist: {0}
vehicle.distance.too.long=Too far away, unable to open
xp-robotaxi-vehicle-convoy-boot/src/main/resources/i18n/messages_zh_CN.properties
@@ -26,4 +26,12 @@
vehicle.not.p.gear=车辆非P档,无法开启后备箱
vehicle.temperature.up=温度最高28℃
vehicle.temperature.down=温度最低22℃
vehicle.code.invalid=车辆编码格式不正确,仅支持5位字母、数字、下划线:{0}
vehicle.code.exists=车辆编码已存在:{0}
vehicle.vin.invalid=车辆VIN码格式不正确,仅支持17位大写字母与数字:{0}
vehicle.vin.exists=车辆VIN码已存在:{0}
vehicle.plate.invalid=车牌号格式不正确:{0}
vehicle.plate.exists=车牌号已存在:{0}
vehicle.color.invalid=车辆颜色长度不能超过2:{0}
vehicle.electronic.not.exists=选择的运营区域不存在:{0}
vehicle.distance.too.long=距离过远,无法打开