puzhibing
2026-04-21 67a1b3997ff33f286a2e91bce3f876c7a301b981
Merge remote-tracking branch 'origin/1.0' into 1.0
9个文件已添加
14个文件已修改
1167 ■■■■■ 已修改文件
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/ElectronicFenceAddDto.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/map/XpGeoFencingPolygon.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/map/XpGeoFencingProhibitedPolygon.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpStation.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/query/operation/XpStationQuery.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/XpStationVo.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpStationManagementController.java 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/exportExcel/XpStationExportExcel.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/mapper/XpStationMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpElectronicFenceService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpPointService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpStationService.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpElectronicFenceServiceImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpStationServiceImpl.java 480 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-map-boot/src/main/resources/mapper/XpStationMapper.xml 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpStationController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/mapper/XpStationMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/IXpElectronicFenceService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/XpStationService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpElectronicFenceServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpStationServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-operation-boot/src/main/resources/mapper/operation/XpStationMapper.xml 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
xp-robotaxi-api/src/main/java/com/xiaopeng/api/dto/operation/ElectronicFenceAddDto.java
@@ -33,7 +33,6 @@
    @ApiModelProperty(value = "生效类型 1=立即生效 2=定时生效")
    @NotNull(message = "生效类型不能为空")
    private Integer effectType;
    @ApiModelProperty(value = "生效时间")
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/map/XpGeoFencingPolygon.java
@@ -34,6 +34,10 @@
     * 围栏区域id
     */
    private String areaId;
    /**
     * 场站id
     */
    private String stationId;
    /**
     * 城市code
@@ -41,7 +45,7 @@
    private String cityCode;
    /**
     * 围栏名称
     * 围栏名称、场站名称
     */
    private String name;
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/map/XpGeoFencingProhibitedPolygon.java
@@ -35,6 +35,10 @@
     */
    private String areaId;
    /**
     * 场站id
     */
    private String stationId;
    /**
xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpStation.java
@@ -37,6 +37,7 @@
    private Long id;
    @ApiModelProperty(value = "场站名称,C端展示主标题")
    @NotNull(message = "场站名称不能为空")
    private String stationName;
    @ApiModelProperty(value = "场站编号唯一 规则:城市首字母-HUB-4位流水号")
@@ -93,6 +94,7 @@
    private BigDecimal stationArea;
    @ApiModelProperty(value = "场站围栏点位 经度_纬度,经度_纬度")
    @NotNull(message = "场站围栏点位不能为空")
    private String fencePoints;
    @ApiModelProperty(value = "中心点经度")
xp-robotaxi-api/src/main/java/com/xiaopeng/api/query/operation/XpStationQuery.java
New file
@@ -0,0 +1,48 @@
package com.xiaopeng.api.query.operation;
import com.xiaopeng.common.core.web.page.BasePage;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
public class XpStationQuery extends BasePage {
    @ApiModelProperty(value = "ids")
    private List<Long> ids;
    @ApiModelProperty(value = "场站名称编码")
    private String stationNameOrNumber;
    @ApiModelProperty(value = "场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效")
    private Integer status;
    @ApiModelProperty(value = "场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效")
    private List<Integer> statusList;
    @ApiModelProperty(value = "创建人")
    private String createBy;
    @ApiModelProperty(value = "创建开始时间")
    private String createStartTime;
    @ApiModelProperty(value = "创建结束时间")
    private String createEndTime;
    @ApiModelProperty(value = "更新人")
    private String updateBy;
    @ApiModelProperty(value = "更新开始时间")
    private String updateStartTime;
    @ApiModelProperty(value = "更新结束时间")
    private String updateEndTime;
    @ApiModelProperty(value = "限停时长")
    private Integer parkingTimeLimitMin;
    @ApiModelProperty(value = "限停时长")
    private Integer parkingTimeLimitMax;
    @ApiModelProperty(value = "开始时间 1正序 2倒序")
    private Integer createSort;
    @ApiModelProperty(value = "更新时间 1正序 2倒序")
    private Integer updateSort;
}
xp-robotaxi-api/src/main/java/com/xiaopeng/api/vo/operation/XpStationVo.java
New file
@@ -0,0 +1,19 @@
package com.xiaopeng.api.vo.operation;
import com.xiaopeng.api.model.operation.XpPoint;
import com.xiaopeng.api.model.operation.XpStation;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
public class XpStationVo extends XpStation {
    @ApiModelProperty(value = "点位设置")
    private List<XpPoint> xpPointList;
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/controller/XpStationManagementController.java
New file
@@ -0,0 +1,198 @@
package com.xiaopeng.map.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import com.xiaopeng.api.model.operation.XpStation;
import com.xiaopeng.api.query.operation.XpStationQuery;
import com.xiaopeng.api.vo.account.LoginUser;
import com.xiaopeng.api.vo.operation.XpStationVo;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.exception.ServiceException;
import com.xiaopeng.common.core.utils.WebUtils;
import com.xiaopeng.common.core.web.page.PageInfo;
import com.xiaopeng.common.security.service.TokenService;
import com.xiaopeng.map.exportExcel.XpStationExportExcel;
import com.xiaopeng.map.service.XpStationService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
@RestController
@Slf4j
@Api(tags = "场站管理")
@RequestMapping("/api/v1/station")
public class XpStationManagementController {
    @Resource
    private XpStationService xpStationService;
    @Resource
    private TokenService tokenService;
    @ApiOperation(value = "场站管理列表", tags = {"管理后台-运营-场站管理"})
    @PostMapping("/pageList")
    public R<PageInfo<XpStation>> pageList(@RequestBody XpStationQuery query) {
        PageInfo<XpStation> pageList = xpStationService.pageList(query);
        return R.ok(pageList);
    }
    /**
     * 新增电子围栏
     */
    @PostMapping("/add")
    @ApiOperation(value = "新增场站管理", tags = {"管理后台-运营-场站管理"})
    public R add(@Valid @RequestBody XpStationVo vo) {
        xpStationService.validate(vo);
        LoginUser loginUser = tokenService.getLoginUser();
        vo.setCreateBy(loginUser.getUsername());
        vo.setUpdateBy(loginUser.getUsername());
        vo.setCreateTime(LocalDateTime.now());
        vo.setUpdateTime(LocalDateTime.now());
        return xpStationService.add(vo);
    }
    /**
     * 编辑电子围栏
     */
    @PostMapping("/edit")
    @ApiOperation(value = "编辑场站管理" , tags = {"管理后台-运营-场站管理"})
    public R edit(@Valid @RequestBody XpStationVo vo) {
        if(vo.getId() == null){
            return R.fail("参数错误");
        }
        xpStationService.validate(vo);
        XpStation bean = xpStationService.getById(vo.getId());
        if(bean == null){
            return R.fail("参数错误");
        }
        LoginUser loginUser = tokenService.getLoginUser();
        vo.setUpdateBy(loginUser.getUsername());
        vo.setUpdateTime(LocalDateTime.now());
        return xpStationService.edit(vo);
    }
    /**
     * 启用中状态: 操作项展示为 [停用]。 停用
     *     -  操作项展示为 [停用]。点击后弹出二次确认框,“当前场站包含12个点位,确认一起停用?
     *       - 是:确认后场站以及子点位状态流转至“已停用”。
     *       - 否:子点位维持当前状态,但自动退回为“独立点位”。
     */
    @ApiOperation(value = "停用" , tags = {"管理后台-运营-场站管理"})
    @PostMapping("/updateStatus/{id}")
    public R updateStatusStop(@PathVariable Long id, @RequestParam Boolean pointStop) {
        XpStation bean = xpStationService.getById(id);
        if (bean == null) {
            throw new ServiceException("场站不存在,请刷新重试");
        }
//        Integer status =6;
//        if(!statusEnable){
//            status = 7;
//        }
        //场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效
        xpStationService.updateStatusStop(bean,pointStop);
        return R.ok();
    }
    /**
     *       - 删除拦截逻辑:
     *         - 测试阶段(待测试、测试中等):点击直接删除。
     *         - 历史运营阶段(已停用、失效):点击弹出二次确认框“当前场站包含12个点位,确认一起删除?”
     *           - 是:点位统一一起删除
     *           - 否:不自动删除点位,点位自动退回为[待测试]的“独立点位”,并自动继承父级运营区的运营时段
     * @param id
     * @param pointDelete
     * @return
     */
    @ApiOperation(value = "删除场站管理", tags = {"管理后台-运营-场站管理"})
    @PostMapping("/delete/{id}")
    public R delete(@PathVariable Long id,@RequestParam Boolean pointDelete) {
        XpStation bean = xpStationService.getById(id);
        if (bean == null) {
            throw new ServiceException("场站不存在,请刷新重试");
        }
        // 场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效
        // 只有状态为已停用(7)时才能删除
        if (bean.getStatus() == 4 || bean.getStatus() == 5 || bean.getStatus() == 6) {
            String msg = "";
            if(bean.getStatus() == 4){
                msg = "可运营";
            }else if (bean.getStatus() == 5){
                msg = "待生效";
            }else {
                msg = "启用中";
            }
            throw new ServiceException("场站"+msg+",无法删除");
        }
        boolean b = xpStationService.removeById(bean,pointDelete);
        if (b){
            return R.ok();
        }
        return R.fail("删除失败");
    }
    @ApiOperation(value = "导出", tags = {"管理后台-运营-场站管理"})
    @PostMapping("/exportExcel")
    public void exportExcel(@RequestBody XpStationQuery query) {
        if (query.getIds().isEmpty()) {
            return;
        }
        List<XpStation> list = xpStationService.findList(query);
        List<XpStationExportExcel> exportList = new ArrayList<>(list.size());
        for (XpStation xpStation : list) {
            XpStationExportExcel excel = new XpStationExportExcel();
            BeanUtils.copyProperties(xpStation, excel);
            excel.setStatusName(xpStation.getStatusName());
            excel.setCreateTimeStr(xpStation.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            excel.setUpdateTimeStr(xpStation.getUpdateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            exportList.add(excel);
        }
        HttpServletResponse response = WebUtils.response();
        if (response == null) {
            throw new ServiceException("导出失败:未获取到响应对象");
        }
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        try {
            String fileName = URLEncoder.encode("场站列表.xls", StandardCharsets.UTF_8.name());
            response.setHeader("Content-disposition", "attachment;filename=" + fileName);
            try (Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), XpStationExportExcel.class, exportList);
                 ServletOutputStream outputStream = response.getOutputStream()) {
                workbook.write(outputStream);
            }
        } catch (IOException e) {
            log.error("点位导出失败 ids={}", query.getIds(), e);
            throw new ServiceException("点位导出失败");
        }
    }
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/exportExcel/XpStationExportExcel.java
New file
@@ -0,0 +1,41 @@
package com.xiaopeng.map.exportExcel;
import cn.afterturn.easypoi.excel.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(value="点位场站")
public class XpStationExportExcel {
    @ApiModelProperty(value = "场站编号唯一 规则:城市首字母-HUB-4位流水号")
    @Excel(name = "场站ID",width = 20)
    private String stationNumber;
    @ApiModelProperty(value = "场站名称,C端展示主标题")
    @Excel(name = "场站名称",width = 20)
    private String stationName;
    @Excel(name = "场站构成",width = 20)
    private String pointsDesc;
    @Excel(name = "状态",width = 20)
    public String statusName;
    /** 更新者 */
    @Excel(name = "更新人",width = 20)
    private String updateBy;
    @Excel(name = "更新时间",width = 20)
    private String updateTimeStr;
    @Excel(name = "创建人",width = 20)
    private String createBy;
    @Excel(name = "创建时间",width = 20)
    private String createTimeStr;
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/mapper/XpStationMapper.java
@@ -2,6 +2,11 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xiaopeng.api.model.operation.XpStation;
import com.xiaopeng.api.query.operation.XpStationQuery;
import com.xiaopeng.common.core.web.page.PageInfo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
@@ -13,4 +18,11 @@
 */
public interface XpStationMapper extends BaseMapper<XpStation> {
    List<XpStation> pageList(@Param("pageInfo") PageInfo<XpStation> pageInfo, @Param("query") XpStationQuery query);
    List<XpStation> pageList(@Param("query") XpStationQuery query);
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpElectronicFenceService.java
@@ -8,6 +8,7 @@
import com.xiaopeng.api.query.operation.ElectronicPageListQuery;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.web.page.PageInfo;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import java.util.List;
@@ -26,6 +27,8 @@
    R editElectronic(ElectronicFenceUpdateDto dto);
    GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr);
    /**
     * 校验围栏名称唯一
     */
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpPointService.java
@@ -19,6 +19,7 @@
import com.xiaopeng.map.vo.NearestVo;
import com.xiaopeng.map.vo.OrderDistanceVo;
import com.xiaopeng.map.vo.StationDetailVo;
import org.springframework.transaction.annotation.Transactional;
import javax.validation.Valid;
import java.time.LocalTime;
@@ -39,6 +40,8 @@
     * 新增点位并根据规则生成点位编号
     */
    XpPoint addPointWithGeneratedNumber(@Valid PointAddDto pointAddDto);
    String getPointNumber(Long electronicAreaId);
    /**
     * 校验点位名称唯一
@@ -101,4 +104,14 @@
    PageInfo<XpPoint> pageList(PointPageListListQuery query);
    List<XpPoint> listByStationId(Long stationId);
    @Transactional(rollbackFor = Exception.class)
    void clearStationInfoByStationId(Long stationId);
    @Transactional(rollbackFor = Exception.class)
    void statusStopByStationId(Long stationId, Boolean pointStop);
    @Transactional(rollbackFor = Exception.class)
    void pointDeleteByStationId(Long stationId, Boolean pointDelete);
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/XpStationService.java
@@ -2,6 +2,15 @@
import com.baomidou.mybatisplus.extension.service.IService;
import com.xiaopeng.api.model.operation.XpStation;
import com.xiaopeng.api.query.operation.XpStationQuery;
import com.xiaopeng.api.vo.operation.XpStationVo;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.web.page.PageInfo;
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType;
import org.apache.shardingsphere.transaction.core.TransactionType;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
 * <p>
@@ -13,4 +22,26 @@
 */
public interface XpStationService extends IService<XpStation> {
    List<XpStation> findList(XpStationQuery query);
    PageInfo<XpStation> pageList(XpStationQuery query);
    void validate(XpStationVo dto);
    @Transactional
    @ShardingSphereTransactionType(TransactionType.BASE)
    R add(XpStationVo dto);
    @Transactional
    @ShardingSphereTransactionType(TransactionType.BASE)
    R edit(XpStationVo dto);
    boolean setStatus(XpStation bean, Integer status);
    boolean updateStatusStop(XpStation bean, Boolean pointStop);
    @Transactional
    boolean removeById(XpStation bean, Boolean pointDelete);
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpElectronicFenceServiceImpl.java
@@ -53,7 +53,6 @@
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.awt.geom.Point2D;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@@ -921,7 +920,8 @@
     * 将围栏点字符串转换为GeoJsonPolygon
     * 格式:经度_维度,经度_维度
     */
    private GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr) {
    @Override
    public GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr) {
        if (fencePointsStr == null || fencePointsStr.trim().isEmpty()) {
            return null;
        }
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpPointServiceImpl.java
@@ -130,9 +130,19 @@
        }
        validatePointNameUnique(pointAddDto.getPointName(), null);
        String cityPrefix = buildCityPrefix(pointAddDto.getElectronicAreaId());
        String lockKey = buildPointNumberLockKey(cityPrefix, POINT_BIZ_TYPE);
        String pointNumber = getPointNumber(pointAddDto.getElectronicAreaId());
        XpPoint xpPoint = new XpPoint();
        BeanUtils.copyProperties(pointAddDto, xpPoint);
        xpPoint.setFpElectronicAreaId(pointAddDto.getElectronicAreaId());
        xpPoint.setPointNumber(pointNumber);
        this.save(xpPoint);
        return xpPoint;
    }
    @Override
    public String getPointNumber(Long electronicAreaId) {
        String cityPrefix = buildCityPrefix(electronicAreaId);
        String lockKey = buildPointNumberLockKey(cityPrefix, POINT_BIZ_TYPE);
        RLock lock = redissonClient.getLock(lockKey);
        boolean locked = false;
        String pointNumber;
@@ -159,13 +169,7 @@
                log.warn("释放点位编号生成锁失败 lockKey={}", lockKey, e);
            }
        }
        XpPoint xpPoint = new XpPoint();
        BeanUtils.copyProperties(pointAddDto, xpPoint);
        xpPoint.setFpElectronicAreaId(pointAddDto.getElectronicAreaId());
        xpPoint.setPointNumber(pointNumber);
        this.save(xpPoint);
        return xpPoint;
        return pointNumber;
    }
    @Override
@@ -674,6 +678,29 @@
            return Collections.emptySet();
        }
    }
    @Override
    public List<XpPoint> listByStationId(Long stationId) {
        if (stationId == null) {
            return Collections.emptyList();
        }
        return this.lambdaQuery()
                .eq(XpPoint::getStationId, stationId)
                .list();
    }
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void clearStationInfoByStationId(Long stationId) {
        if (stationId == null) {
            return;
        }
        this.lambdaUpdate()
                .eq(XpPoint::getStationId, stationId)
                .set(XpPoint::getStationId, null)
                .set(XpPoint::getStationSortNo, null)
                .update();
    }
    /**
     * 查询用户收藏的点位ID。
@@ -695,4 +722,52 @@
        }
    }
    /**
     * 场站停用
     *     -  操作项展示为 [停用]。点击后弹出二次确认框,“当前场站包含12个点位,确认一起停用?
     *       - 是:确认后场站以及子点位状态流转至“已停用”。
     *       - 否:子点位维持当前状态,但自动退回为“独立点位”。
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void statusStopByStationId(Long stationId, Boolean pointStop) {
        if (stationId == null) {
            return;
        }
        if(pointStop){
            this.lambdaUpdate()
                    .eq(XpPoint::getStationId, stationId)
                    .set(XpPoint::getOperationStatus, 3)//运营状态 1=待启用 2=启用中 3=已停用 4=已失效
                    .update();
        }else{
            clearStationInfoByStationId(stationId);
        }
    }
    /**
     * 场站删除
     *       - 删除拦截逻辑:
     *         - 测试阶段(待测试、测试中等):点击直接删除。
     *         - 历史运营阶段(已停用、失效):点击弹出二次确认框“当前场站包含12个点位,确认一起删除?”
     *           - 是:点位统一一起删除
     *           - 否:不自动删除点位,点位自动退回为[待测试]的“独立点位”,并自动继承父级运营区的运营时段
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void pointDeleteByStationId(Long stationId, Boolean pointDelete) {
        if (stationId == null) {
            return;
        }
        if(pointDelete){
            this.lambdaUpdate()
                    .eq(XpPoint::getStationId, stationId)
                    .set(XpPoint::getDeleted, 1)
                    .update();
        }else{
            clearStationInfoByStationId(stationId);
        }
    }
}
xp-robotaxi-map-boot/src/main/java/com/xiaopeng/map/service/serviceimpl/XpStationServiceImpl.java
@@ -1,10 +1,41 @@
package com.xiaopeng.map.service.serviceimpl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xiaopeng.api.model.operation.XpStation;
import com.xiaopeng.api.model.map.XpGeoFencingPolygon;
import com.xiaopeng.api.model.operation.*;
import com.xiaopeng.api.query.operation.XpStationQuery;
import com.xiaopeng.api.vo.operation.XpStationVo;
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.exception.ServiceException;
import com.xiaopeng.common.core.web.page.PageInfo;
import com.xiaopeng.map.constant.I18nKeyConstant;
import com.xiaopeng.map.mapper.XpRegionMapper;
import com.xiaopeng.map.mapper.XpStationMapper;
import com.xiaopeng.map.service.XpElectronicFenceAreaService;
import com.xiaopeng.map.service.XpElectronicFenceService;
import com.xiaopeng.map.service.XpPointService;
import com.xiaopeng.map.service.XpStationService;
import com.xiaopeng.map.utils.GDMapGeocodingUtil;
import com.xiaopeng.map.utils.PolygonOverlapService;
import net.sourceforge.pinyin4j.PinyinHelper;
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType;
import org.apache.shardingsphere.transaction.core.TransactionType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
 * <p>
@@ -17,4 +48,451 @@
@Service
public class XpStationServiceImpl extends ServiceImpl<XpStationMapper, XpStation> implements XpStationService {
    @Autowired
    private MongoTemplate mongoTemplate;
    @Resource
    private PolygonOverlapService polygonOverlapService;
    @Resource
    private XpElectronicFenceService xpElectronicFenceService;
    @Resource
    private XpElectronicFenceAreaService xpElectronicFenceAreaService;
    @Autowired
    private GDMapGeocodingUtil gdMapGeocodingUtil;
    @Autowired
    private XpRegionMapper xpRegionMapper;
    @Resource
    private XpPointService xpPointService;
    @Override
    public List<XpStation> findList(XpStationQuery query) {
        return this.baseMapper.pageList( query);
    }
    @Override
    public PageInfo<XpStation> pageList(XpStationQuery query) {
        PageInfo<XpStation> pageInfo = new PageInfo<>(query.getPageNum(), query.getPageSize());
        if (null == query.getCreateSort() && null == query.getUpdateSort()){
            query.setCreateSort(2);
        }
        List<XpStation> list = this.baseMapper.pageList(pageInfo, query);
        if (list.isEmpty()) {
            return new PageInfo<>();
        }
//        List<Long> electronicIds = list.stream().map(XpElectronicFence::getId).collect(Collectors.toList());
//        Map<Long, List<XpElectronicFenceArea>> electronicFenceAreaMap = xpElectronicFenceAreaMapper.selectList(new LambdaQueryWrapper<XpElectronicFenceArea>()
//                        .in(XpElectronicFenceArea::getElectronicId, electronicIds)).stream()
//                .collect(Collectors.groupingBy(XpElectronicFenceArea::getElectronicId));
        for (XpStation bean : list) {
            if(bean.getFencePoints() !=null){
                bean.setPointCount(bean.getFencePoints().split(",").length); //点位总数
            }
            bean.setOperatingPointCount(0);//运营中点位数
        }
        pageInfo.setRecords(list);
        return pageInfo;
    }
    public List<XpStation> findList() {
        // 获取现有的运营区域(areaType=1)
        LambdaQueryWrapper<XpStation> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(XpStation::getDeleted, 0);
        //status = 场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效
        queryWrapper.in(XpStation::getStatus, 1,2,3,4,5,6,7);
        return super.list(queryWrapper);
    }
    /**
     * 判断一个坐标点位是否在一个坐标区域内
     * @param longitude 经度
     * @param latitude 纬度
     * @param fencePoints 围栏点位字符串,格式为 "lng1_lat1,lng2_lat2,..."
     * @return 是否在区域内 true在 false不在
     */
    public boolean isPointInPolygon(double longitude, double latitude, String fencePoints) {
        if (fencePoints == null || fencePoints.trim().isEmpty()) {
            return false;
        }
        // 解析围栏点位
        String[] points = fencePoints.split(",");
        if (points.length < 3) {
            return false;
        }
        int n = points.length;
        boolean inside = false;
        double x = longitude, y = latitude;
        double[] xs = new double[n];
        double[] ys = new double[n];
        for (int i = 0; i < n; i++) {
            String[] coords = points[i].split("_");
            if (coords.length != 2) {
                continue;
            }
            try {
                xs[i] = Double.parseDouble(coords[0]);
                ys[i] = Double.parseDouble(coords[1]);
            } catch (NumberFormatException e) {
                return false;
            }
        }
        // 使用射线法判断点是否在多边形内
        for (int i = 0, j = n - 1; i < n; j = i++) {
            if (((ys[i] > y) != (ys[j] > y)) &&
                    (x < (xs[j] - xs[i]) * (y - ys[i]) / (ys[j] - ys[i]) + xs[i])) {
                inside = !inside;
            }
        }
        return inside;
    }
    /**
     * 判断一个坐标区域(多边形A)是否完全在另一个坐标区域(多边形B)内
     * 原理:如果多边形A的所有顶点都在多边形B内,则认为A在B内
     * @param innerFencePoints 内部围栏点位字符串(待判断是否在外的围栏),格式为 "lng1_lat1,lng2_lat2,..."
     * @param outerFencePoints 外部围栏点位字符串(参考围栏),格式为 "lng1_lat1,lng2_lat2,..."
     * @return 是否在内 true在内 false不在
     */
    public boolean isPolygonInPolygon(String innerFencePoints, String outerFencePoints) {
        if (innerFencePoints == null || innerFencePoints.trim().isEmpty()
                || outerFencePoints == null || outerFencePoints.trim().isEmpty()) {
            return false;
        }
        // 解析内部围栏点位
        String[] innerPoints = innerFencePoints.split(",");
        if (innerPoints.length < 3) {
            return false;
        }
        // 检查内部围栏的每一个顶点是否都在外部围栏内
        for (String pointStr : innerPoints) {
            String[] coords = pointStr.split("_");
            if (coords.length != 2) {
                continue;
            }
            try {
                double lon = Double.parseDouble(coords[0]);
                double lat = Double.parseDouble(coords[1]);
                // 如果有一个顶点不在外部围栏内,则整个区域不在内部
                if (!isPointInPolygon(lon, lat, outerFencePoints)) {
                    return false;
                }
            } catch (NumberFormatException e) {
                return false;
            }
        }
        return true;
    }
    /**
     * 根据围栏点位字符串计算中心点坐标
     * @param fencePoints 围栏点位字符串,格式为 "lng1_lat1,lng2_lat2,..."
     * @return double[0] 经度, double[1] 纬度
     */
    public double[] calculateCenterPoint(String fencePoints) {
        if (fencePoints == null || fencePoints.trim().isEmpty()) {
            return new double[]{0.0, 0.0};
        }
        String[] points = fencePoints.split(",");
        if (points.length == 0) {
            return new double[]{0.0, 0.0};
        }
        double sumLon = 0.0;
        double sumLat = 0.0;
        int count = 0;
        for (String point : points) {
            String[] coords = point.split("_");
            if (coords.length == 2) {
                try {
                    double lon = Double.parseDouble(coords[0]);
                    double lat = Double.parseDouble(coords[1]);
                    sumLon += lon;
                    sumLat += lat;
                    count++;
                } catch (NumberFormatException e) {
                    // 忽略解析错误的点
                }
            }
        }
        if (count == 0) {
            return new double[]{0.0, 0.0};
        }
        double centerLon = sumLon / count;
        double centerLat = sumLat / count;
        return new double[]{centerLon, centerLat};
    }
    @Override
    public void validate(XpStationVo dto) {
        if (dto.getElectronicAreaId() == null) {
            throw new ServiceException("运营区域ID不能为空");
        }
        if (dto.getElectronicId() == null) {
            throw new ServiceException("围栏ID不能为空");
        }
//        if(dto.getXpPointList().isEmpty()){
//            throw new ServiceException("场站点位不能为空");
//        }
        for (XpPoint xpPoint : dto.getXpPointList()) {
            if(StrUtil.isBlank(xpPoint.getPointName()) ){
                throw new ServiceException("点位名称不能为空");
            }
            if(StrUtil.isBlank(xpPoint.getPointPoi()) ){
                throw new ServiceException("点位poi不能为空");
            }
            if(StrUtil.isBlank(xpPoint.getPointAddress())){
                throw new ServiceException("点位地址不能为空");
            }
            if(xpPoint.getPointLon() == null || xpPoint.getPointLat() == null){
                throw new ServiceException("点位经纬度不能为空");
            }
        }
        if (dto.getEffectTime().isBefore(LocalDateTime.now())) {
            throw new ServiceException("计划生效时间不能小于当前时间");
        }
        XpElectronicFence electronicFence = xpElectronicFenceService.getById(dto.getElectronicId());
        if(electronicFence == null){
            throw new ServiceException("场站id错误");
        }
        XpElectronicFenceArea electronicFenceArea = xpElectronicFenceAreaService.getById(dto.getElectronicAreaId());
        if(electronicFenceArea == null){
            throw new ServiceException("所属运营区id错误");
        }
        dto.setCityCode(electronicFence.getCityCode());
        double[] centerPoints = calculateCenterPoint(dto.getFencePoints());//场站中心点位
        dto.setCenterLon(centerPoints[0]);
        dto.setCenterLat(centerPoints[1]);
        //判断场站是否在 围栏里
        boolean polygonInPolygon = isPolygonInPolygon(electronicFenceArea.getFencePoints(), dto.getFencePoints());
        if(!polygonInPolygon){
            throw new ServiceException("场站不在 "+electronicFence.getElectronicName()+"围栏内");
        }
        //判断点位是否在 场站里
        if(!dto.getXpPointList().isEmpty()) {
            for (XpPoint xpPoint : dto.getXpPointList()) {
                if(!isPointInPolygon(xpPoint.getPointLon(),xpPoint.getPointLat(),dto.getFencePoints())){
                    throw new ServiceException(xpPoint.getPointName()+" 点位不在 "+dto.getStationName()+"场站内");
                }
            }
        }
        //获取现有的场站区域
        List<XpStation> listAll = findList();
        // 检查新区域与现有运营区域是否有重叠
        for (XpStation existingArea : listAll) {
            if (polygonOverlapService.checkPolygonOverlap(dto.getFencePoints(),existingArea.getFencePoints())) {
                throw new ServiceException("场站区域{}与现有场站区域{}发生重叠",I18nKeyConstant.EDIT_ELECTRONIC_ERROR,
                        new Object[]{dto.getStationName(), existingArea.getStationName()}
                );
            }
        }
    }
    public 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();
    }
    @Transactional
    @ShardingSphereTransactionType(TransactionType.BASE)
    @Override
    public R add(XpStationVo dto) {
        try {
            // 生成围栏id 规则:城市首字母-类型简写-4位自增流水号(如 SZ-OP-001),支持复制;数据停用或删除后,流水号不回收
            XpRegion xpRegion = xpRegionMapper.selectOne(new LambdaQueryWrapper<XpRegion>()
                    .eq(XpRegion::getCode, dto.getCityCode()));
            String name = xpRegion.getName();
            dto.setCityName(name);
            dto.setStatus(0); //status = 场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效
            this.save(dto);
            //场站编号唯一 规则:城市首字母-HUB-4位流水号
            // 根据城市中文名,截取前两位的首字母并且大写
            // 截取前两个字符
            String prefix = name.substring(0, 2);
            // 获取拼音首字母并大写
            String result = getPinyinInitials(prefix).toUpperCase();
            dto.setStationNumber(result + "-HUB-" + String.format("%04d", dto.getId()));
            this.updateById(dto);
            save_operatingArea(dto);
            savePoint(dto);
            return R.ok();
        } catch (Exception e) {
            log.error("新增场站失败", e);
            throw new ServiceException("新增场站失败");
        }
    }
    @Transactional
    @ShardingSphereTransactionType(TransactionType.BASE)
    @Override
    public R edit(XpStationVo dto) {
        try {
//            dto.setStatus(0); //status = 场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效
            this.updateById(dto);
            // 删除旧的区域数据,然后保存新的区域数据
            remove_operatingArea(dto);
            save_operatingArea(dto);
            xpPointService.clearStationInfoByStationId(dto.getId());
            savePoint(dto);
            return R.ok();
        } catch (Exception e) {
            log.error("新增场站失败", e);
            throw new ServiceException("新增场站失败");
        }
    }
    @Transactional
    public void savePoint(XpStationVo dto) {
        if(CollUtil.isNotEmpty(dto.getXpPointList())){
            //保存点位
            List<XpPoint> xpPointListAdd = new ArrayList<>();
            List<XpPoint> xpPointListUpdate = new ArrayList<>();
            int stationSortNo = 1;
            for (XpPoint xpPoint : dto.getXpPointList()) {
                xpPoint.setStationSortNo(stationSortNo++);
                xpPoint.setStationId(dto.getId());
                if(xpPoint.getId() == null){
                    xpPointService.getPointNumber(dto.getElectronicAreaId());
                    xpPoint.setFpElectronicAreaId(dto.getElectronicAreaId());
                    xpPoint.setElectronicAreaId(dto.getElectronicAreaId());
                    xpPointListAdd.add(xpPoint);
                }else{
                    xpPointListUpdate.add(xpPoint);
                }
            }
            if(CollUtil.isNotEmpty(xpPointListAdd)){
                xpPointService.saveBatch(xpPointListAdd);
            }
            if(CollUtil.isNotEmpty(xpPointListUpdate)){
                xpPointService.updateBatchById(xpPointListUpdate);
            }
        }
    }
    @Override
    public boolean setStatus(XpStation bean, Integer status){
        bean.setStatus(status);
        boolean b = updateById(bean);
        if(b){
            //场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效
            List<Integer> statusList = Arrays.asList(4, 5, 6, 7) ;
            if(statusList.contains(status) ){
                save_operatingArea(bean);
            }
            if(status == 7 ){
                remove_operatingArea(bean);
            }
        }
        return b;
    }
    private void remove_operatingArea(XpStation bean) {
        // 同步删除Mongo电子围栏多边形
        Query delMongoQuery = new Query();
        delMongoQuery.addCriteria(Criteria.where("stationId").is(String.valueOf(bean.getId())));
        mongoTemplate.remove(delMongoQuery, XpGeoFencingPolygon.class);
    }
    private void save_operatingArea (XpStation bean) {
        XpGeoFencingPolygon xpGeoFencingPolygon = new XpGeoFencingPolygon();
        xpGeoFencingPolygon.setFenceId(String.valueOf(bean.getElectronicId()));
        xpGeoFencingPolygon.setName(bean.getStationName());
        xpGeoFencingPolygon.setAreaId(String.valueOf(bean.getElectronicAreaId()));
        xpGeoFencingPolygon.setCityCode(bean.getCityCode());
        xpGeoFencingPolygon.setStationId(String.valueOf(bean.getId()));
        xpGeoFencingPolygon.setOperationStrategy(null);//运营策略
        // 将围栏点转换为GeoJSON多边形
        GeoJsonPolygon geoJsonPolygon = xpElectronicFenceService.convertToGeoJsonPolygon(bean.getFencePoints());
        xpGeoFencingPolygon.setPoints(geoJsonPolygon);
        mongoTemplate.insert(xpGeoFencingPolygon);
    }
    /**
     * 停用
     *     -  操作项展示为 [停用]。点击后弹出二次确认框,“当前场站包含12个点位,确认一起停用?
     *       - 是:确认后场站以及子点位状态流转至“已停用”。
     *       - 否:子点位维持当前状态,但自动退回为“独立点位”。
     */
    @Override
    public boolean updateStatusStop(XpStation bean, Boolean pointStop){
        xpPointService.statusStopByStationId(bean.getId(),pointStop);
        return setStatus(bean, 7);
    }
    /**
     *       - 删除拦截逻辑:
     *         - 测试阶段(待测试、测试中等):点击直接删除。
     *         - 历史运营阶段(已停用、失效):点击弹出二次确认框“当前场站包含12个点位,确认一起删除?”
     *           - 是:点位统一一起删除
     *           - 否:不自动删除点位,点位自动退回为[待测试]的“独立点位”,并自动继承父级运营区的运营时段
     * @param bean 实体
     * @return
     */
    @Transactional
    @Override
    public boolean removeById(XpStation bean, Boolean pointDelete){
        boolean b = true;
        //场站状态 0=草稿 1=待测试 2=测试中 3=测试不通过 4=可运营 5=待生效 6=启用中 7=已停用 8=失效
        if(bean.getStatus() == 1 || bean.getStatus() == 2 || bean.getStatus() == 3 || bean.getStatus() == 7 || bean.getStatus() == 8){
            b = super.removeById(bean.getId());
        }
        if(bean.getStatus() == 7 || bean.getStatus() == 8){
             xpPointService.pointDeleteByStationId(bean.getId(),pointDelete);
        }
        if(b){
            // 同步删除Mongo电子围栏多边形
            Query delMongoQuery = new Query();
            delMongoQuery.addCriteria(Criteria.where("stationId").is(String.valueOf(bean.getId())));
            mongoTemplate.remove(delMongoQuery, XpGeoFencingPolygon.class);
        }
        return b;
    }
}
xp-robotaxi-map-boot/src/main/resources/mapper/XpStationMapper.xml
@@ -40,4 +40,84 @@
        sort_no, is_deleted, create_time, update_time, create_by, update_by
    </sql>
    <select id="pageList" resultType="com.xiaopeng.api.model.operation.XpStation">
        select a.*,
            (select count(1) from xp_point b where b.station_id = a.id) point_count,
            (select count(1) from xp_point c where c.station_id = a.id and c.operation_status = 2) operatingPointCount
        from xp_station a
        inner join xp_electronic_fence b on a.electronic_id = b.id and b.is_deleted = 0
        where a.is_deleted = 0
          <if test="query.ids != null">
            and a.id in
            <foreach item="id" collection="query.ids" open="(" separator="," close=")">
                #{id}
            </foreach>
        </if>
        <if test="query.stationNameOrNumber != null and query.stationNameOrNumber != ''">
            and( a.station_name like concat('%',#{query.stationNameOrNumber},'%')
            or a.station_number like concat('%',#{query.stationNameOrNumber},'%')
            )
        </if>
        <if test="query.status != null">
            and a.status = #{query.status}
        </if>
        <if test="query.statusList != null">
            <foreach collection="query.statusList" item="status" open=" and a.status in (" separator="," close=")">
                #{status}
            </foreach>
        </if>
        <if test="query.parkingTimeLimitMin != null">
            and a.parking_time_limit &gt;= #{query.parkingTimeLimitMin}
        </if>
        <if test="query.parkingTimeLimitMax != null">
            and a.parking_time_limit &lt;= #{query.parkingTimeLimitMax}
        </if>
        <if test="query.createBy != null and query.createBy!=''">
            and a.create_by like concat('%',#{query.createBy},'%')
        </if>
        <if test="query.createStartTime != null and query.createStartTime!=''">
            and a.create_time >= #{query.createStartTime}
        </if>
        <if test="query.createEndTime != null and query.createEndTime!=''">
            and #{query.createEndTime} >= a.create_time
        </if>
        <if test="query.updateBy != null and query.updateBy!=''">
            and a.update_by like concat('%',#{query.updateBy},'%')
        </if>
        <if test="query.updateStartTime != null and query.updateStartTime!=''">
            and a.update_time >= #{query.updateStartTime}
        </if>
        <if test="query.updateEndTime != null and query.updateEndTime!=''">
            and #{query.updateEndTime} >= a.update_time
        </if>
        <choose>
            <when test="query.createSort == 1 or query.createSort == 2 or query.updateSort == 1 or query.updateSort == 2">
                <trim prefix="order by" suffixOverrides=",">
                    <if test="query.createSort == 1">
                        a.create_time asc,
                    </if>
                    <if test="query.createSort == 2">
                        a.create_time desc,
                    </if>
                    <if test="query.updateSort == 1">
                        a.update_time asc,
                    </if>
                    <if test="query.updateSort == 2">
                        a.update_time desc,
                    </if>
                </trim>
            </when>
            <otherwise>
                order by a.update_time desc
            </otherwise>
        </choose>
    </select>
</mapper>
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/controller/XpStationController.java
New file
@@ -0,0 +1,19 @@
package com.xiaopeng.operation.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * 场站
 */
@RestController
@RequestMapping
public class XpStationController {
}
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/mapper/XpStationMapper.java
New file
@@ -0,0 +1,24 @@
package com.xiaopeng.operation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xiaopeng.api.model.operation.XpStation;
import com.xiaopeng.api.query.operation.XpStationQuery;
import com.xiaopeng.common.core.web.page.PageInfo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
 * 场站 Mapper 接口
 * </p>
 *
 * @author mitao
 * @since 2026-04-12
 */
public interface XpStationMapper extends BaseMapper<XpStation> {
}
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/IXpElectronicFenceService.java
@@ -7,6 +7,7 @@
import com.xiaopeng.api.dto.operation.ElectronicFenceAddDto;
import com.xiaopeng.api.dto.operation.ElectronicFenceUpdateDto;
import com.xiaopeng.api.query.operation.ElectronicPageListQuery;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import java.util.List;
@@ -22,10 +23,14 @@
    PageInfo<XpElectronicFence> pageList(ElectronicPageListQuery query);
    String getPinyinInitials(String chinese);
    R addElectronic(ElectronicFenceAddDto dto);
    R editElectronic(ElectronicFenceUpdateDto dto);
    GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr);
    /**
     * 查询启用的电子围栏列表
     * @return
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/XpStationService.java
New file
@@ -0,0 +1,18 @@
package com.xiaopeng.operation.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xiaopeng.api.model.operation.XpStation;
/**
 * <p>
 * 场站 服务类
 * </p>
 *
 * @author mitao
 * @since 2026-04-12
 */
public interface XpStationService extends IService<XpStation> {
}
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpElectronicFenceServiceImpl.java
@@ -16,7 +16,6 @@
import com.xiaopeng.common.core.domain.R;
import com.xiaopeng.common.core.exception.ServiceException;
import com.xiaopeng.common.core.utils.DateUtils;
import com.xiaopeng.common.core.utils.StringUtils;
import com.xiaopeng.common.core.utils.bean.BeanUtils;
import com.xiaopeng.common.core.web.page.PageInfo;
import com.xiaopeng.operation.constant.I18nKeyConstant;
@@ -105,7 +104,8 @@
        pageInfo.setRecords(list);
        return pageInfo;
    }
    public static String getPinyinInitials(String chinese) {
    @Override
    public String getPinyinInitials(String chinese) {
        StringBuilder pinyinInitials = new StringBuilder();
        for (char c : chinese.toCharArray()) {
            // 获取拼音数组(多音字会返回多个拼音)
@@ -602,7 +602,8 @@
     * 将围栏点字符串转换为GeoJsonPolygon
     * 格式:经度_维度,经度_维度
     */
    private GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr) {
    @Override
    public GeoJsonPolygon convertToGeoJsonPolygon(String fencePointsStr) {
        if (fencePointsStr == null || fencePointsStr.trim().isEmpty()) {
            return null;
        }
xp-robotaxi-operation-boot/src/main/java/com/xiaopeng/operation/service/impl/XpStationServiceImpl.java
New file
@@ -0,0 +1,22 @@
package com.xiaopeng.operation.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xiaopeng.api.model.operation.XpStation;
import com.xiaopeng.operation.mapper.XpStationMapper;
import com.xiaopeng.operation.service.XpStationService;
import org.springframework.stereotype.Service;
/**
 * <p>
 * 场站 服务实现类
 * </p>
 *
 * @author mitao
 * @since 2026-04-12
 */
@Service
public class XpStationServiceImpl extends ServiceImpl<XpStationMapper, XpStation> implements XpStationService {
}
xp-robotaxi-operation-boot/src/main/resources/mapper/operation/XpStationMapper.xml
New file
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaopeng.operation.mapper.XpStationMapper">
    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, electronic_name, electronic_number, status, effect_type, effect_time, failure_type, failure_time, operation_strategy, is_deleted, create_time, update_time, create_by, update_by
    </sql>
    <select id="pageList" resultType="com.xiaopeng.api.model.operation.XpStation">
        select a.*
        from xp_station a
        where a.is_deleted = 0
        <choose>
            <when test="query.createSort == 1 or query.createSort == 2 or query.updateSort == 1 or query.updateSort == 2">
                <trim prefix="order by" suffixOverrides=",">
                    <if test="query.createSort == 1">
                        a.create_time asc,
                    </if>
                    <if test="query.createSort == 2">
                        a.create_time desc,
                    </if>
                    <if test="query.updateSort == 1">
                        a.update_time asc,
                    </if>
                    <if test="query.updateSort == 2">
                        a.update_time desc,
                    </if>
                </trim>
            </when>
            <otherwise>
                order by a.update_time desc
            </otherwise>
        </choose>
    </select>
</mapper>