集成百度鹰眼服务AK

This commit is contained in:
xuqiuyun
2025-11-21 17:22:20 +08:00
parent 73051df002
commit 0f963bf535
57 changed files with 6036 additions and 685 deletions

View File

@@ -7,11 +7,13 @@ import com.aiotagro.cattletrade.business.dto.DeliveryAddDto;
import com.aiotagro.cattletrade.business.dto.DeliveryCreateDto;
import com.aiotagro.cattletrade.business.dto.DeliveryEditDto;
import com.aiotagro.cattletrade.business.dto.DeliveryQueryDto;
import com.aiotagro.cattletrade.business.dto.DeliveryTrackQueryDto;
import com.aiotagro.cattletrade.business.dto.DeviceAssignDto;
import com.aiotagro.cattletrade.business.entity.Delivery;
import com.aiotagro.cattletrade.business.entity.DeliveryDevice;
import com.aiotagro.cattletrade.business.entity.JbqClient;
import com.aiotagro.cattletrade.business.entity.XqClient;
import com.aiotagro.cattletrade.business.service.BaiduYingyanService;
import com.aiotagro.cattletrade.business.service.IDeliveryService;
import com.aiotagro.cattletrade.business.service.IDeliveryDeviceService;
import com.aiotagro.cattletrade.business.service.IJbqClientService;
@@ -85,6 +87,9 @@ public class DeliveryController {
@Autowired
private OrderMapper orderMapper;
@Autowired
private BaiduYingyanService baiduYingyanService;
/**
@@ -594,8 +599,45 @@ public class DeliveryController {
if (existDelivery == null) {
return AjaxResult.error("运单不存在");
}
if (status == 2) {
if (StringUtils.isBlank(existDelivery.getLicensePlate())) {
return AjaxResult.error("车牌号为空,无法开启运输状态");
}
// 去除车牌号中的空格,确保格式一致
String licensePlate = existDelivery.getLicensePlate().trim();
String entityName = StringUtils.defaultIfBlank(existDelivery.getYingyanEntityName(), licensePlate);
// 如果终端名称与车牌号不一致,使用车牌号(去除空格)
if (!licensePlate.equals(entityName)) {
entityName = licensePlate;
}
boolean ensureResult = baiduYingyanService.ensureEntity(entityName);
Delivery syncInfo = new Delivery();
syncInfo.setId(id);
syncInfo.setYingyanEntityName(entityName);
if (existDelivery.getYingyanLastSyncTime() == null) {
Date syncStart = existDelivery.getEstimatedDeliveryTime();
if (syncStart == null) {
syncStart = existDelivery.getEstimatedDepartureTime();
}
if (syncStart == null) {
syncStart = existDelivery.getCreateTime();
}
syncInfo.setYingyanLastSyncTime(syncStart);
}
deliveryService.updateById(syncInfo);
if (!ensureResult) {
logger.warn("运单 {} 创建百度鹰眼终端失败,将在后台重试", existDelivery.getDeliveryNumber());
}
} else if (status == 3) {
Delivery arrivalUpdate = new Delivery();
arrivalUpdate.setId(id);
arrivalUpdate.setArrivalTime(new Date());
deliveryService.updateById(arrivalUpdate);
}
Delivery delivery = new Delivery();
delivery.setId(id);
delivery.setStatus(status);
@@ -614,6 +656,15 @@ public class DeliveryController {
}
}
/**
* 查询运送清单百度鹰眼轨迹/停留点
*/
@SaCheckPermission("delivery:view")
@PostMapping("/yingyan/track")
public AjaxResult getYingyanTrack(@Validated @RequestBody DeliveryTrackQueryDto dto) {
return deliveryService.queryYingyanTrack(dto.getDeliveryId());
}
/**
* 删除装车订单(物理删除)
* 删除订单时同时清空关联设备的delivery_id和weight
@@ -671,7 +722,7 @@ public class DeliveryController {
/**
* 逻辑删除运送清单
* 只标记为已删除,不清空设备绑定关系,保留历史记录
* 逻辑删除时同时清空关联设备的delivery_id和car_number
*/
@SaCheckPermission("delivery:delete")
@PostMapping("/deleteLogic")
@@ -694,12 +745,36 @@ public class DeliveryController {
return AjaxResult.error("无权限删除此运单");
}
// 1. 先清空关联设备的delivery_id和car_number
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.aiotagro.cattletrade.business.entity.IotDeviceData> queryWrapper =
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
queryWrapper.eq("delivery_id", id);
List<com.aiotagro.cattletrade.business.entity.IotDeviceData> devices = iotDeviceDataMapper.selectList(queryWrapper);
// 使用 MyBatis-Plus 的逻辑删除功能
int updatedCount = 0;
for (com.aiotagro.cattletrade.business.entity.IotDeviceData device : devices) {
// 使用LambdaUpdateWrapper确保null值也能被更新
com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<com.aiotagro.cattletrade.business.entity.IotDeviceData> updateWrapper =
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<>();
updateWrapper.eq(com.aiotagro.cattletrade.business.entity.IotDeviceData::getDeviceId, device.getDeviceId())
.set(com.aiotagro.cattletrade.business.entity.IotDeviceData::getDeliveryId, null)
.set(com.aiotagro.cattletrade.business.entity.IotDeviceData::getCarNumber, null)
.set(com.aiotagro.cattletrade.business.entity.IotDeviceData::getUpdateTime, new Date());
int result = iotDeviceDataMapper.update(null, updateWrapper);
if (result > 0) {
updatedCount++;
logger.debug("清空设备绑定信息: deviceId={}, delivery_id=null, car_number=null", device.getDeviceId());
}
}
logger.info("逻辑删除运送清单,已清空 {} 个设备的绑定信息运单ID: {}", updatedCount, id);
// 2. 使用 MyBatis-Plus 的逻辑删除功能
boolean deleted = deliveryService.removeById(id);
if (deleted) {
return AjaxResult.success("运单删除成功");
return AjaxResult.success("运单删除成功,已清空 " + updatedCount + " 个设备的绑定信息");
} else {
return AjaxResult.error("运单删除失败");
}

View File

@@ -53,13 +53,19 @@ public class IotDeviceProxyController {
// 只查询正常状态的设备is_delet=0
queryWrapper.and(wrapper -> wrapper.eq("is_delet", 0).or().isNull("is_delet"));
// 根据设备类型查询(用于创建运送清单时过滤设备)
// 根据设备类型查询
if (params.containsKey("type") && params.get("type") != null) {
Integer deviceType = (Integer) params.get("type");
queryWrapper.eq("device_type", deviceType);
// 创建运送清单时,只显示未绑定的设备(delivery_id为空
queryWrapper.isNull("delivery_id");
logger.info("查询未绑定的设备,类型: {}", deviceType);
// 只有当 allotType 参数存在且为 "unassigned" 时,才限制 delivery_id 为空
// 这是为了在创建运送清单时只显示未绑定的设备
// 硬件管理页面eartag.vue, host.vue, collar.vue不传递 allotType会显示所有设备
if (params.containsKey("allotType") && "unassigned".equals(params.get("allotType"))) {
queryWrapper.isNull("delivery_id");
logger.info("查询未绑定的设备,类型: {}", deviceType);
} else {
logger.info("查询所有设备(包括已分配的),类型: {}", deviceType);
}
}
// 根据设备ID查询

View File

@@ -237,6 +237,7 @@ public class MemberController {
*/
@SaCheckPermission("member:edit")
@PostMapping("/updateDriver")
@Transactional
public AjaxResult updateDriver(@RequestBody Map<String, Object> params) {
try {
Integer id = (Integer) params.get("id");
@@ -246,6 +247,7 @@ public class MemberController {
// 获取参数值
String username = (String) params.get("username");
String mobile = (String) params.get("mobile");
String driverLicense = (String) params.get("driverLicense");
String drivingLicense = (String) params.get("drivingLicense");
String carImg = (String) params.get("carImg");
@@ -253,7 +255,48 @@ public class MemberController {
String idCard = (String) params.get("idCard");
String remark = (String) params.get("remark");
// 执行更新不再包含carNumber参数)
// 获取司机的member_id
Map<String, Object> driverInfo = memberDriverMapper.selectDriverById(id);
if (driverInfo == null) {
return AjaxResult.error("司机信息不存在");
}
// member_id 从数据库返回时可能是Long类型需要转换为Integer
Object memberIdObj = driverInfo.get("member_id");
Integer memberId = null;
if (memberIdObj != null) {
if (memberIdObj instanceof Long) {
memberId = ((Long) memberIdObj).intValue();
} else if (memberIdObj instanceof Integer) {
memberId = (Integer) memberIdObj;
} else if (memberIdObj instanceof Number) {
memberId = ((Number) memberIdObj).intValue();
}
}
if (memberId == null) {
return AjaxResult.error("司机关联的会员ID不存在");
}
// 如果提供了新手机号,检查是否与其他用户重复
if (mobile != null && !mobile.trim().isEmpty()) {
String currentMobile = (String) driverInfo.get("mobile");
// 如果手机号有变化,检查新手机号是否已存在
if (currentMobile == null || !currentMobile.equals(mobile.trim())) {
Integer existingMemberId = memberMapper.selectMemberIdByMobile(mobile.trim());
if (existingMemberId != null && !existingMemberId.equals(memberId)) {
return AjaxResult.error("该手机号已被其他用户使用");
}
}
// 更新member表的手机号
int mobileUpdateResult = memberMapper.updateMemberMobile(memberId, mobile.trim());
if (mobileUpdateResult <= 0) {
return AjaxResult.error("更新手机号失败");
}
}
// 执行更新member_driver表
int result = memberDriverMapper.updateDriver(id, username, driverLicense,
drivingLicense, carImg, recordCode, idCard, remark);
@@ -288,6 +331,14 @@ public class MemberController {
String cbkAccount = (String) params.get("cbkAccount");
String remark = (String) params.get("remark");
// 处理mobile和cbkAccount如果为空字符串设置为null
if (mobile != null && mobile.trim().isEmpty()) {
mobile = null;
}
if (cbkAccount != null && cbkAccount.trim().isEmpty()) {
cbkAccount = null;
}
// 执行更新
int result = memberMapper.updateUserInfo(id, mobile, type, status, username, cbkAccount, remark);
@@ -318,9 +369,6 @@ public class MemberController {
String remark = (String) params.get("remark");
// 参数验证
if (mobile == null || mobile.trim().isEmpty()) {
return AjaxResult.error("手机号不能为空");
}
if (username == null || username.trim().isEmpty()) {
return AjaxResult.error("用户姓名不能为空");
}
@@ -328,10 +376,17 @@ public class MemberController {
return AjaxResult.error("用户类型不能为空");
}
// 检查手机号是否已存在直接查询member表
Integer existingMemberId = memberMapper.selectMemberIdByMobile(mobile);
if (existingMemberId != null) {
return AjaxResult.error("该手机号已存在");
// 处理mobile如果为空或空字符串设置为null
if (mobile != null && mobile.trim().isEmpty()) {
mobile = null;
}
// 检查手机号是否已存在仅在mobile不为空时检查
if (mobile != null && !mobile.trim().isEmpty()) {
Integer existingMemberId = memberMapper.selectMemberIdByMobile(mobile);
if (existingMemberId != null) {
return AjaxResult.error("该手机号已存在");
}
}
// 先插入member表
@@ -340,8 +395,15 @@ public class MemberController {
return AjaxResult.error("用户基础信息插入失败");
}
// 获取刚插入的member记录的ID(通过查询最新记录)
Integer memberId = memberMapper.selectMemberIdByMobile(mobile);
// 获取刚插入的member记录的ID
Integer memberId = null;
if (mobile != null && !mobile.trim().isEmpty()) {
// 如果mobile不为空通过mobile查询
memberId = memberMapper.selectMemberIdByMobile(mobile);
} else {
// 如果mobile为空查询最新插入的记录通过create_time和id
memberId = memberMapper.selectLatestMemberId();
}
if (memberId == null) {
return AjaxResult.error("获取新用户ID失败");
}

View File

@@ -131,5 +131,29 @@ public class OrderController {
return AjaxResult.error("查询订单详情失败:" + e.getMessage());
}
}
/**
* 批量导入订单
*/
@SaCheckPermission("order:import")
@PostMapping("/batchImport")
public AjaxResult batchImport(@RequestBody Map<String, Object> params) {
try {
logger.info("批量导入订单,参数:{}", params);
@SuppressWarnings("unchecked")
List<Map<String, Object>> orders = (List<Map<String, Object>>) params.get("orders");
if (orders == null || orders.isEmpty()) {
logger.error("批量导入失败:订单列表不能为空");
return AjaxResult.error("订单列表不能为空");
}
return orderService.batchImportOrders(orders);
} catch (Exception e) {
logger.error("批量导入订单失败:{}", e.getMessage(), e);
return AjaxResult.error("批量导入订单失败:" + e.getMessage());
}
}
}

View File

@@ -27,6 +27,9 @@ public class DeliveryEditDto {
private String driverMobile;
/** 车牌号 */
private String licensePlate;
private Integer buyerId;
private Double buyerPrice;

View File

@@ -0,0 +1,16 @@
package com.aiotagro.cattletrade.business.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 运单轨迹查询参数
*/
@Data
public class DeliveryTrackQueryDto {
@NotNull(message = "运单ID不能为空")
private Integer deliveryId;
}

View File

@@ -0,0 +1,30 @@
package com.aiotagro.cattletrade.business.dto.yingyan;
import lombok.Data;
/**
* 百度鹰眼停留点
*/
@Data
public class YingyanStayPoint {
private double latitude;
private double longitude;
/**
* 停留开始时间Unix 秒)
*/
private long startTime;
/**
* 停留结束时间Unix 秒)
*/
private long endTime;
/**
* 停留时长(秒)
*/
private long duration;
}

View File

@@ -0,0 +1,30 @@
package com.aiotagro.cattletrade.business.dto.yingyan;
import lombok.Data;
/**
* 百度鹰眼轨迹点
*/
@Data
public class YingyanTrackPoint {
private double latitude;
private double longitude;
/**
* 轨迹点定位时间Unix 秒)
*/
private long locTime;
/**
* 米/秒
*/
private Double speed;
/**
* 航向角
*/
private Double direction;
}

View File

@@ -100,7 +100,7 @@ public class Delivery implements Serializable {
private Double firmPrice;
/**
* 状态1-待装车2-已装车/预付款已支付3-已装车/尾款待支付4-已核验/待买家付款5-尾款已付款6-发票待开/进项票7-发票待开/销项
* 状态1-准备中2-运输中3-已结束
*/
@TableField("status")
private Integer status;
@@ -179,6 +179,26 @@ public class Delivery implements Serializable {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date estimatedDeliveryTime;
/**
* 百度鹰眼终端名称(默认为车牌)
*/
@TableField("yingyan_entity_name")
private String yingyanEntityName;
/**
* 百度鹰眼最后同步时间
*/
@TableField("yingyan_last_sync_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date yingyanLastSyncTime;
/**
* 实际到达时间(鹰眼自动回写)
*/
@TableField("arrival_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date arrivalTime;
/**
* 登记智能耳标数
*/

View File

@@ -25,4 +25,12 @@ public interface JbqClientLogMapper extends BaseMapper<JbqClientLog> {
* 批量插入耳标日志数据
*/
int batchInsert(@Param("list") List<JbqClientLog> logList);
/**
* 供百度鹰眼同步使用的增量查询
*/
List<JbqClientLog> listLogsForYingyan(@Param("deviceIds") List<String> deviceIds,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime,
@Param("limit") Integer limit);
}

View File

@@ -26,4 +26,12 @@ public interface JbqServerLogMapper extends BaseMapper<JbqServerLog> {
* 批量插入主机日志数据
*/
int batchInsert(@Param("list") List<JbqServerLog> logList);
/**
* 查询用于百度鹰眼推送的主机轨迹
*/
List<JbqServerLog> listLogsForYingyan(@Param("deviceIds") List<String> deviceIds,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime,
@Param("limit") Integer limit);
}

View File

@@ -188,6 +188,12 @@ public interface MemberMapper extends BaseMapper<Member> {
@Select("SELECT id FROM member WHERE mobile = #{mobile} ORDER BY id DESC LIMIT 1")
Integer selectMemberIdByMobile(@Param("mobile") String mobile);
/**
* 查询最新插入的member记录ID用于mobile为null时获取新插入的ID
*/
@Select("SELECT id FROM member ORDER BY id DESC LIMIT 1")
Integer selectLatestMemberId();
/**
* 新增用户基础信息插入member表
*/
@@ -215,4 +221,24 @@ public interface MemberMapper extends BaseMapper<Member> {
"LEFT JOIN member_user mu ON m.id = mu.member_id " +
"WHERE m.id = #{memberId}")
Map<String, Object> selectMemberUserById(@Param("memberId") Integer memberId);
/**
* 批量根据member ID查询member和member_user关联数据
*/
@Select("<script>" +
"SELECT m.id, m.mobile, mu.username " +
"FROM member m " +
"LEFT JOIN member_user mu ON m.id = mu.member_id " +
"WHERE m.id IN " +
"<foreach collection='memberIds' item='id' open='(' separator=',' close=')'>" +
" #{id}" +
"</foreach>" +
"</script>")
List<Map<String, Object>> selectMemberUserByIds(@Param("memberIds") List<Integer> memberIds);
/**
* 更新member表的手机号
*/
@org.apache.ibatis.annotations.Update("UPDATE member SET mobile = #{mobile}, update_time = NOW() WHERE id = #{memberId}")
int updateMemberMobile(@Param("memberId") Integer memberId, @Param("mobile") String mobile);
}

View File

@@ -25,4 +25,12 @@ public interface XqClientLogMapper extends BaseMapper<XqClientLog> {
* 批量插入项圈日志数据
*/
int batchInsert(@Param("list") List<XqClientLog> logList);
/**
* 查询用于百度鹰眼推送的项圈轨迹
*/
List<XqClientLog> listLogsForYingyan(@Param("deviceIds") List<String> deviceIds,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime,
@Param("limit") Integer limit);
}

View File

@@ -0,0 +1,611 @@
package com.aiotagro.cattletrade.business.service;
import com.aiotagro.cattletrade.business.dto.yingyan.YingyanStayPoint;
import com.aiotagro.cattletrade.business.dto.yingyan.YingyanTrackPoint;
import com.aiotagro.common.core.constant.BaiduYingyanConstants;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.time.Duration;
import java.util.Date;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 百度鹰眼 API 封装
*/
@Service
public class BaiduYingyanService {
private static final Logger logger = LoggerFactory.getLogger(BaiduYingyanService.class);
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
public BaiduYingyanService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.restTemplate = buildRestTemplate();
}
/**
* 确保终端存在(幂等)
* 返回值true-终端存在或创建成功false-创建失败(终端不存在且创建失败)
*/
public boolean ensureEntity(String entityName) {
if (StringUtils.isBlank(entityName)) {
logger.warn("ensureEntity 失败:终端名称为空");
return false;
}
// ✅ 验证 entityName 不是 "entity" 字符串
if ("entity".equals(entityName) || "entity_name".equals(entityName)) {
logger.error("ensureEntity 失败entityName 参数错误,值为 '{}',这可能是参数传递错误", entityName);
return false;
}
try {
MultiValueMap<String, String> form = baseForm();
form.add("entity_name", entityName);
// entity_desc 为非必填项,且命名规则限制:只支持中文、英文字母、下划线、连字符、数字
// 为避免参数错误,不传递 entity_desc 参数
logger.debug("确保终端存在 - entity={}", entityName);
JsonNode result = postForm("/entity/add", form);
int status = result.path("status").asInt(-1);
String message = result.path("message").asText();
// ✅ 修复status=0创建成功、3005终端已存在、3006其他成功状态都视为成功
if (status == 0) {
logger.info("✅ 鹰眼终端创建成功, entityName={}", entityName);
return true;
} else if (status == 3005) {
// ✅ 终端已存在,这是正常情况,视为成功
logger.info("✅ 鹰眼终端已存在, entityName={}, message={}", entityName, message);
return true;
} else if (status == 3006) {
logger.info("✅ 鹰眼终端操作成功, entityName={}, status={}", entityName, status);
return true;
}
// 其他状态码视为失败
logger.warn("❌ 鹰眼创建终端失败, entityName={}, status={}, message={}",
entityName, status, message);
} catch (Exception e) {
logger.error("❌ 鹰眼创建终端异常, entityName={}", entityName, e);
}
return false;
}
/**
* 推送单条轨迹点
*/
public boolean pushTrackPoint(String entityName, double latitude, double longitude, long locTime) {
if (StringUtils.isBlank(entityName)) {
logger.warn("鹰眼上传轨迹失败:终端名称为空");
return false;
}
// ✅ 验证经纬度有效性
if (latitude == 0 && longitude == 0) {
logger.warn("鹰眼上传轨迹失败:经纬度无效 (0,0), entity={}", entityName);
return false;
}
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
logger.warn("鹰眼上传轨迹失败:经纬度超出有效范围, entity={}, lat={}, lon={}",
entityName, latitude, longitude);
return false;
}
try {
MultiValueMap<String, String> form = baseForm();
form.add("entity_name", entityName);
form.add("latitude", String.valueOf(latitude));
form.add("longitude", String.valueOf(longitude));
form.add("loc_time", String.valueOf(locTime));
form.add("coord_type_input", "wgs84");
logger.debug("鹰眼上传轨迹点 - entity={}, lat={}, lon={}, locTime={}",
entityName, latitude, longitude, locTime);
JsonNode result = postForm("/track/addpoint", form);
int status = result.path("status").asInt(-1);
String message = result.path("message").asText();
if (status == 0) {
logger.debug("鹰眼上传轨迹成功 - entity={}, lat={}, lon={}",
entityName, latitude, longitude);
return true;
}
// ✅ 详细记录失败原因
logger.warn("鹰眼上传轨迹失败 - entity={}, status={}, message={}, lat={}, lon={}, locTime={}",
entityName, status, message, latitude, longitude, locTime);
// 如果是常见错误,记录更详细的信息
if (status == 3001) {
logger.error("鹰眼上传轨迹失败:参数错误,请检查经纬度和时间格式");
} else if (status == 3002) {
logger.error("鹰眼上传轨迹失败:服务不存在或未启用");
} else if (status == 3003) {
logger.error("鹰眼上传轨迹失败:终端不存在");
} else if (status == 3004) {
logger.error("鹰眼上传轨迹失败:轨迹点时间格式错误");
}
} catch (Exception e) {
logger.error("鹰眼上传轨迹异常 - entity={}, lat={}, lon={}, locTime={}",
entityName, latitude, longitude, locTime, e);
}
return false;
}
/**
* 查询轨迹单次查询限制24小时内
*/
public List<YingyanTrackPoint> queryTrack(String entityName, long startTime, long endTime) {
if (StringUtils.isBlank(entityName)) {
return Collections.emptyList();
}
try {
Map<String, Object> params = new HashMap<>();
params.put("entity_name", entityName);
params.put("start_time", startTime);
params.put("end_time", endTime);
params.put("is_processed", 1);
params.put("need_denoise", 1);
params.put("need_mapmatch", 0);
params.put("coord_type_output", "bd09ll");
logger.debug("查询轨迹 - entity={}, startTime={} ({}), endTime={} ({})",
entityName, startTime, new Date(startTime * 1000), endTime, new Date(endTime * 1000));
JsonNode result = get("/track/gettrack", params);
int status = result.path("status").asInt(-1);
String message = result.path("message").asText();
if (!isSuccess(result)) {
// ✅ status=3003 可能是"终端不存在"或"指定时间范围内无轨迹数据"
// 如果终端确实存在(通过 ensureEntity 确认),则可能是时间范围内无数据
if (status == 3003) {
logger.debug("鹰眼查询轨迹:指定时间范围内可能无轨迹数据, entity={}, startTime={}, endTime={}, message={}",
entityName, new Date(startTime * 1000), new Date(endTime * 1000), message);
} else {
logger.warn("鹰眼查询轨迹失败, entity={}, status={}, message={}, startTime={}, endTime={}",
entityName, status, message, new Date(startTime * 1000), new Date(endTime * 1000));
}
return Collections.emptyList();
}
JsonNode pointsNode = result.path("track").path("points");
if (pointsNode == null || !pointsNode.isArray() || pointsNode.size() == 0) {
return Collections.emptyList();
}
List<YingyanTrackPoint> points = new ArrayList<>(pointsNode.size());
pointsNode.forEach(node -> {
if (!node.hasNonNull("latitude") || !node.hasNonNull("longitude")) {
return;
}
YingyanTrackPoint point = new YingyanTrackPoint();
point.setLatitude(node.path("latitude").asDouble());
point.setLongitude(node.path("longitude").asDouble());
point.setLocTime(node.path("loc_time").asLong());
if (node.has("speed")) {
point.setSpeed(node.path("speed").asDouble());
}
if (node.has("direction")) {
point.setDirection(node.path("direction").asDouble());
}
points.add(point);
});
return points;
} catch (Exception e) {
logger.error("鹰眼查询轨迹异常, entity={}", entityName, e);
return Collections.emptyList();
}
}
/**
* 分段查询轨迹支持超过24小时的查询
* 按照24小时为间隔分段查询然后拼接结果
*
* @param entityName 终端名称
* @param startTime 开始时间(秒级时间戳)
* @param endTime 结束时间(秒级时间戳,不能超过当前时间)
* @return 拼接后的轨迹点列表,按时间排序
*/
public List<YingyanTrackPoint> queryTrackSegmented(String entityName, long startTime, long endTime) {
if (StringUtils.isBlank(entityName)) {
return Collections.emptyList();
}
// ✅ 首先确保终端存在
if (!ensureEntity(entityName)) {
logger.warn("查询轨迹前终端不存在或创建失败,无法查询 - entity={}", entityName);
return Collections.emptyList();
}
// 验证时间范围
long currentTime = System.currentTimeMillis() / 1000;
if (endTime > currentTime) {
logger.warn("结束时间不能超过当前时间自动调整为当前时间。entity={}, endTime={}, currentTime={}",
entityName, endTime, currentTime);
endTime = currentTime;
}
if (startTime >= endTime) {
logger.warn("开始时间不能大于等于结束时间。entity={}, startTime={}, endTime={}",
entityName, startTime, endTime);
return Collections.emptyList();
}
// 计算时间差(秒)
long timeDiff = endTime - startTime;
// 24小时 = 86400秒
final long SEGMENT_INTERVAL = 86400L;
// 如果时间差小于等于24小时直接查询
if (timeDiff <= SEGMENT_INTERVAL) {
logger.debug("时间范围在24小时内直接查询。entity={}, startTime={}, endTime={}, diff={}秒",
entityName, startTime, endTime, timeDiff);
return queryTrack(entityName, startTime, endTime);
}
// 需要分段查询
logger.info("开始分段查询轨迹 - entity={}, startTime={}, endTime={}, 总时长={}小时",
entityName, startTime, endTime, timeDiff / 3600);
List<YingyanTrackPoint> allPoints = new ArrayList<>();
long segmentStart = startTime;
int segmentIndex = 1;
while (segmentStart < endTime) {
// 计算当前段的结束时间不超过endTime且不超过24小时
long segmentEnd = Math.min(segmentStart + SEGMENT_INTERVAL, endTime);
logger.debug("查询第{}段轨迹 - entity={}, segmentStart={}, segmentEnd={}, 时长={}小时",
segmentIndex, entityName, segmentStart, segmentEnd, (segmentEnd - segmentStart) / 3600);
try {
// ✅ 在查询前确保终端存在(每段都检查,因为可能在某些时间段终端还未创建)
if (!ensureEntity(entityName)) {
logger.warn("第{}段查询前终端不存在或创建失败,跳过该段 - entity={}", segmentIndex, entityName);
segmentStart = segmentEnd;
segmentIndex++;
continue;
}
List<YingyanTrackPoint> segmentPoints = queryTrack(entityName, segmentStart, segmentEnd);
if (!segmentPoints.isEmpty()) {
allPoints.addAll(segmentPoints);
logger.debug("第{}段查询成功,获得{}个轨迹点", segmentIndex, segmentPoints.size());
} else {
logger.debug("第{}段无轨迹点", segmentIndex);
}
} catch (Exception e) {
logger.error("第{}段轨迹查询异常 - entity={}, segmentStart={}, segmentEnd={}",
segmentIndex, entityName, segmentStart, segmentEnd, e);
// 继续查询下一段,不中断
}
// 移动到下一段(下一段的开始时间 = 当前段的结束时间)
segmentStart = segmentEnd;
segmentIndex++;
// 避免无限循环
if (segmentIndex > 100) {
logger.error("分段查询超过100段可能存在逻辑错误停止查询。entity={}", entityName);
break;
}
}
// 按时间排序并去重基于locTime
if (!allPoints.isEmpty()) {
// 按时间排序
allPoints.sort(Comparator.comparingLong(YingyanTrackPoint::getLocTime));
// 去重:相同时间戳的轨迹点只保留一个
List<YingyanTrackPoint> uniquePoints = new ArrayList<>();
long lastLocTime = -1;
for (YingyanTrackPoint point : allPoints) {
if (point.getLocTime() != lastLocTime) {
uniquePoints.add(point);
lastLocTime = point.getLocTime();
}
}
logger.info("分段查询完成 - entity={}, 总段数={}, 原始轨迹点数={}, 去重后轨迹点数={}",
entityName, segmentIndex - 1, allPoints.size(), uniquePoints.size());
return uniquePoints;
}
logger.warn("分段查询未获得任何轨迹点 - entity={}, startTime={}, endTime={}",
entityName, startTime, endTime);
return Collections.emptyList();
}
/**
* 查询停留点单次查询限制24小时内
*/
public List<YingyanStayPoint> queryStayPoints(String entityName, long startTime, long endTime, int stayTimeSeconds) {
if (StringUtils.isBlank(entityName)) {
logger.warn("查询停留点失败:终端名称为空");
return Collections.emptyList();
}
// ✅ 验证 entityName 不是 "entity" 字符串
if ("entity".equals(entityName) || "entity_name".equals(entityName)) {
logger.error("查询停留点失败entityName 参数错误,值为 '{}',这可能是参数传递错误", entityName);
return Collections.emptyList();
}
try {
Map<String, Object> params = new HashMap<>();
params.put("entity_name", entityName);
params.put("start_time", startTime);
params.put("end_time", endTime);
params.put("stay_time", stayTimeSeconds);
params.put("coord_type_output", "bd09ll");
logger.debug("查询停留点 - entity={}, startTime={} ({}), endTime={} ({}), stayTimeSeconds={}",
entityName, startTime, new Date(startTime * 1000), endTime, new Date(endTime * 1000), stayTimeSeconds);
JsonNode result = get("/analysis/staypoint", params);
int status = result.path("status").asInt(-1);
String message = result.path("message").asText();
if (!isSuccess(result)) {
// ✅ status=3003 可能是"终端不存在"或"指定时间范围内无停留点数据"
// 如果终端确实存在(通过 ensureEntity 确认),则可能是时间范围内无数据
if (status == 3003) {
logger.debug("鹰眼查询停留点:指定时间范围内可能无停留点数据, entity={}, startTime={}, endTime={}, message={}",
entityName, new Date(startTime * 1000), new Date(endTime * 1000), message);
} else {
logger.warn("鹰眼查询停留点失败, entity={}, status={}, message={}, startTime={}, endTime={}",
entityName, status, message, new Date(startTime * 1000), new Date(endTime * 1000));
}
return Collections.emptyList();
}
JsonNode stayNode = result.path("stay_points");
if (stayNode == null || !stayNode.isArray() || stayNode.size() == 0) {
return Collections.emptyList();
}
List<YingyanStayPoint> stayPoints = new ArrayList<>(stayNode.size());
stayNode.forEach(node -> {
if (!node.hasNonNull("latitude") || !node.hasNonNull("longitude")) {
return;
}
YingyanStayPoint stayPoint = new YingyanStayPoint();
stayPoint.setLatitude(node.path("latitude").asDouble());
stayPoint.setLongitude(node.path("longitude").asDouble());
stayPoint.setStartTime(node.path("start_time").asLong());
stayPoint.setEndTime(node.path("end_time").asLong());
stayPoint.setDuration(node.path("duration").asLong());
stayPoints.add(stayPoint);
});
return stayPoints;
} catch (Exception e) {
logger.error("鹰眼查询停留点异常, entity={}", entityName, e);
return Collections.emptyList();
}
}
/**
* 分段查询停留点支持超过24小时的查询
* 按照24小时为间隔分段查询然后拼接结果
*
* @param entityName 终端名称
* @param startTime 开始时间(秒级时间戳)
* @param endTime 结束时间(秒级时间戳,不能超过当前时间)
* @param stayTimeSeconds 停留时间阈值(秒)
* @return 拼接后的停留点列表,按开始时间排序
*/
public List<YingyanStayPoint> queryStayPointsSegmented(String entityName, long startTime, long endTime, int stayTimeSeconds) {
if (StringUtils.isBlank(entityName)) {
return Collections.emptyList();
}
// ✅ 首先确保终端存在
if (!ensureEntity(entityName)) {
logger.warn("查询停留点前终端不存在或创建失败,无法查询 - entity={}", entityName);
return Collections.emptyList();
}
// 验证时间范围
long currentTime = System.currentTimeMillis() / 1000;
if (endTime > currentTime) {
logger.warn("结束时间不能超过当前时间自动调整为当前时间。entity={}, endTime={}, currentTime={}",
entityName, endTime, currentTime);
endTime = currentTime;
}
if (startTime >= endTime) {
logger.warn("开始时间不能大于等于结束时间。entity={}, startTime={}, endTime={}",
entityName, startTime, endTime);
return Collections.emptyList();
}
// 计算时间差(秒)
long timeDiff = endTime - startTime;
// 24小时 = 86400秒
final long SEGMENT_INTERVAL = 86400L;
// 如果时间差小于等于24小时直接查询
if (timeDiff <= SEGMENT_INTERVAL) {
logger.debug("时间范围在24小时内直接查询停留点。entity={}, startTime={}, endTime={}, diff={}秒",
entityName, startTime, endTime, timeDiff);
return queryStayPoints(entityName, startTime, endTime, stayTimeSeconds);
}
// 需要分段查询
logger.info("开始分段查询停留点 - entity={}, startTime={}, endTime={}, 总时长={}小时",
entityName, startTime, endTime, timeDiff / 3600);
List<YingyanStayPoint> allStayPoints = new ArrayList<>();
long segmentStart = startTime;
int segmentIndex = 1;
while (segmentStart < endTime) {
// 计算当前段的结束时间不超过endTime且不超过24小时
long segmentEnd = Math.min(segmentStart + SEGMENT_INTERVAL, endTime);
logger.debug("查询第{}段停留点 - entity={}, segmentStart={}, segmentEnd={}, 时长={}小时",
segmentIndex, entityName, segmentStart, segmentEnd, (segmentEnd - segmentStart) / 3600);
try {
// ✅ 在查询前确保终端存在(每段都检查,因为可能在某些时间段终端还未创建)
if (!ensureEntity(entityName)) {
logger.warn("第{}段查询前终端不存在或创建失败,跳过该段 - entity={}", segmentIndex, entityName);
segmentStart = segmentEnd;
segmentIndex++;
continue;
}
List<YingyanStayPoint> segmentStayPoints = queryStayPoints(entityName, segmentStart, segmentEnd, stayTimeSeconds);
if (!segmentStayPoints.isEmpty()) {
allStayPoints.addAll(segmentStayPoints);
logger.debug("第{}段查询成功,获得{}个停留点", segmentIndex, segmentStayPoints.size());
} else {
logger.debug("第{}段无停留点", segmentIndex);
}
} catch (Exception e) {
logger.error("第{}段停留点查询异常 - entity={}, segmentStart={}, segmentEnd={}",
segmentIndex, entityName, segmentStart, segmentEnd, e);
// 继续查询下一段,不中断
}
// 移动到下一段(下一段的开始时间 = 当前段的结束时间)
segmentStart = segmentEnd;
segmentIndex++;
// 避免无限循环
if (segmentIndex > 100) {
logger.error("分段查询超过100段可能存在逻辑错误停止查询。entity={}", entityName);
break;
}
}
// 按开始时间排序并去重基于startTime
if (!allStayPoints.isEmpty()) {
// 按开始时间排序
allStayPoints.sort(Comparator.comparingLong(YingyanStayPoint::getStartTime));
// 去重:相同开始时间的停留点只保留一个
List<YingyanStayPoint> uniqueStayPoints = new ArrayList<>();
long lastStartTime = -1;
for (YingyanStayPoint stayPoint : allStayPoints) {
if (stayPoint.getStartTime() != lastStartTime) {
uniqueStayPoints.add(stayPoint);
lastStartTime = stayPoint.getStartTime();
}
}
logger.info("分段查询停留点完成 - entity={}, 总段数={}, 原始停留点数={}, 去重后停留点数={}",
entityName, segmentIndex - 1, allStayPoints.size(), uniqueStayPoints.size());
return uniqueStayPoints;
}
logger.warn("分段查询未获得任何停留点 - entity={}, startTime={}, endTime={}",
entityName, startTime, endTime);
return Collections.emptyList();
}
private JsonNode postForm(String path, MultiValueMap<String, String> form) throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(form, headers);
ResponseEntity<String> response = restTemplate.postForEntity(buildUrl(path), httpEntity, String.class);
return parseBody(response.getBody());
}
private JsonNode get(String path, Map<String, Object> params) throws Exception {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(buildUrl(path))
.queryParam("ak", BaiduYingyanConstants.AK)
.queryParam("service_id", BaiduYingyanConstants.SERVICE_ID);
if (params != null) {
params.forEach((key, value) -> {
if (value != null) {
// ✅ 验证参数名和值,避免参数传递错误
if ("entity_name".equals(key) && ("entity".equals(value) || "entity_name".equals(value))) {
logger.error("参数传递错误entity_name 的值不能是 '{}',这可能是参数名和值混淆了", value);
throw new IllegalArgumentException("entity_name 参数值错误: " + value);
}
builder.queryParam(key, value);
}
});
}
// ✅ 记录请求URL调试用生产环境可关闭
String requestUrl = builder.toUriString();
logger.debug("百度鹰眼API请求 - path={}, url={}", path, requestUrl.replaceAll("ak=[^&]+", "ak=***"));
String response = restTemplate.getForObject(requestUrl, String.class);
return parseBody(response);
}
private JsonNode parseBody(String body) throws Exception {
if (StringUtils.isBlank(body)) {
return objectMapper.createObjectNode();
}
return objectMapper.readTree(body);
}
private boolean isSuccess(JsonNode node) {
if (node == null) {
return false;
}
int status = node.path("status").asInt(-1);
return status == 0;
}
private MultiValueMap<String, String> baseForm() {
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add("ak", BaiduYingyanConstants.AK);
form.add("service_id", String.valueOf(BaiduYingyanConstants.SERVICE_ID));
return form;
}
private RestTemplate buildRestTemplate() {
org.springframework.http.client.SimpleClientHttpRequestFactory factory =
new org.springframework.http.client.SimpleClientHttpRequestFactory();
factory.setConnectTimeout((int) Duration.ofSeconds(10).toMillis());
factory.setReadTimeout((int) Duration.ofSeconds(30).toMillis());
return new RestTemplate(factory);
}
private String buildUrl(String path) {
if (StringUtils.isBlank(path)) {
return BaiduYingyanConstants.BASE_URL;
}
if (path.startsWith("http")) {
return path;
}
if (!path.startsWith("/")) {
path = "/" + path;
}
return BaiduYingyanConstants.BASE_URL + path;
}
}

View File

@@ -0,0 +1,426 @@
package com.aiotagro.cattletrade.business.service;
import com.aiotagro.cattletrade.business.dto.yingyan.YingyanTrackPoint;
import com.aiotagro.cattletrade.business.entity.Delivery;
import com.aiotagro.cattletrade.business.entity.IotDeviceData;
import com.aiotagro.cattletrade.business.entity.JbqClientLog;
import com.aiotagro.cattletrade.business.entity.JbqServerLog;
import com.aiotagro.cattletrade.business.entity.XqClientLog;
import com.aiotagro.cattletrade.business.mapper.IotDeviceDataMapper;
import com.aiotagro.cattletrade.business.mapper.JbqClientLogMapper;
import com.aiotagro.cattletrade.business.mapper.JbqServerLogMapper;
import com.aiotagro.cattletrade.business.mapper.XqClientLogMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 运送清单鹰眼同步服务
*/
@Service
public class DeliveryYingyanSyncService {
private static final Logger logger = LoggerFactory.getLogger(DeliveryYingyanSyncService.class);
/**
* 每次最多同步 120 个轨迹点,避免请求过多
*/
private static final int MAX_SYNC_POINTS = 120;
private static final double ARRIVAL_RADIUS_METERS = 500D;
@Autowired
private IDeliveryService deliveryService;
@Autowired
private IotDeviceDataMapper iotDeviceDataMapper;
@Autowired
private JbqClientLogMapper jbqClientLogMapper;
@Autowired
private JbqServerLogMapper jbqServerLogMapper;
@Autowired
private XqClientLogMapper xqClientLogMapper;
@Autowired
private BaiduYingyanService baiduYingyanService;
/**
* 同步所有运输中的运送清单
*/
public void syncActiveDeliveries() {
logger.info("========== 开始执行百度鹰眼轨迹同步任务 ==========");
LambdaQueryWrapper<Delivery> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Delivery::getStatus, 2);
wrapper.isNotNull(Delivery::getLicensePlate);
List<Delivery> deliveries = deliveryService.list(wrapper);
if (CollectionUtils.isEmpty(deliveries)) {
logger.info("未找到运输中的运单status=2跳过同步");
return;
}
logger.info("找到 {} 个运输中的运单,开始同步轨迹", deliveries.size());
int successCount = 0;
int failCount = 0;
int noPointsCount = 0;
for (Delivery delivery : deliveries) {
try {
int result = syncDelivery(delivery);
if (result > 0) {
successCount++;
logger.info("运单 {} 同步成功,上传了 {} 个轨迹点", delivery.getDeliveryNumber(), result);
} else if (result == 0) {
noPointsCount++;
logger.debug("运单 {} 无有效轨迹点可同步", delivery.getDeliveryNumber());
} else {
failCount++;
}
} catch (Exception e) {
failCount++;
logger.error("运单 {} 同步百度鹰眼失败", delivery.getDeliveryNumber(), e);
}
}
logger.info("百度鹰眼轨迹同步完成 - 成功: {}, 无轨迹点: {}, 失败: {}", successCount, noPointsCount, failCount);
logger.info("========== 百度鹰眼轨迹同步任务执行完成 ==========");
}
private int syncDelivery(Delivery delivery) {
String entityName = resolveEntityName(delivery);
if (StringUtils.isBlank(entityName)) {
logger.warn("运单 {} 无法同步,车牌为空", delivery.getDeliveryNumber());
return -1;
}
logger.info("开始同步运单 {} 的轨迹,终端名称: {}", delivery.getDeliveryNumber(), entityName);
// ✅ 确保终端存在(如果已存在则继续,如果不存在则创建)
boolean entityExists = baiduYingyanService.ensureEntity(entityName);
if (!entityExists) {
logger.warn("运单 {} 创建/确保终端失败,终端名称: {},但继续尝试上传轨迹点(终端可能已存在)",
delivery.getDeliveryNumber(), entityName);
// 注意:即使 ensureEntity 返回 false也继续尝试上传轨迹点
// 因为终端可能已经存在,只是创建时返回了错误状态码
} else {
logger.debug("运单 {} 终端已存在或创建成功,终端名称: {}", delivery.getDeliveryNumber(), entityName);
}
Date startTime = determineSyncStart(delivery);
Date endTime = new Date();
logger.info("运单 {} 查询轨迹点时间范围: {} 至 {}",
delivery.getDeliveryNumber(), startTime, endTime);
List<YingyanTrackPoint> points = collectTrackPoints(delivery, startTime, endTime);
if (CollectionUtils.isEmpty(points)) {
logger.warn("运单 {} 未找到有效轨迹点,终端名称: {}, 时间范围: {} 至 {}",
delivery.getDeliveryNumber(), entityName, startTime, endTime);
// ✅ 详细排查原因:从 iot_device_data 表查询设备
QueryWrapper<IotDeviceData> deviceQueryWrapper = new QueryWrapper<>();
deviceQueryWrapper.eq("delivery_id", delivery.getId());
deviceQueryWrapper.and(wrapper -> wrapper.eq("is_delet", 0).or().isNull("is_delet"));
List<IotDeviceData> devices = iotDeviceDataMapper.selectList(deviceQueryWrapper);
logger.warn("运单 {} 在 iot_device_data 表中的设备数量: {}",
delivery.getDeliveryNumber(), devices != null ? devices.size() : 0);
if (devices != null && !devices.isEmpty()) {
devices.forEach(device -> {
logger.warn("运单 {} 设备信息 - deviceId: {}, deviceType: {}, latitude: {}, longitude: {}",
delivery.getDeliveryNumber(), device.getDeviceId(), device.getDeviceType(),
device.getLatitude(), device.getLongitude());
});
} else {
logger.warn("运单 {} 在 iot_device_data 表中没有找到设备deliveryId: {}",
delivery.getDeliveryNumber(), delivery.getId());
}
return 0;
}
logger.info("运单 {} 找到 {} 个有效轨迹点,开始上传到百度鹰眼",
delivery.getDeliveryNumber(), points.size());
int successCount = 0;
int failCount = 0;
for (YingyanTrackPoint point : points) {
boolean success = baiduYingyanService.pushTrackPoint(
entityName, point.getLatitude(), point.getLongitude(), point.getLocTime());
if (success) {
successCount++;
Date locDate = new Date(point.getLocTime() * 1000L);
updateLastSync(delivery, locDate);
handleArrivalIfNeeded(delivery, point);
if (successCount <= 3 || successCount % 20 == 0) {
logger.debug("运单 {} 上传轨迹点成功 [{}/{}] - 纬度: {}, 经度: {}, 时间: {}",
delivery.getDeliveryNumber(), successCount, points.size(),
point.getLatitude(), point.getLongitude(), locDate);
}
} else {
failCount++;
if (failCount <= 3) {
logger.warn("运单 {} 上传轨迹点失败 [{}/{}] - 纬度: {}, 经度: {}, 时间: {}",
delivery.getDeliveryNumber(), failCount, points.size(),
point.getLatitude(), point.getLongitude(),
new Date(point.getLocTime() * 1000L));
}
}
if (delivery.getStatus() != null && delivery.getStatus() == 3) {
logger.info("运单 {} 状态已变为已结束,停止同步", delivery.getDeliveryNumber());
break;
}
}
logger.info("运单 {} 轨迹上传完成 - 成功: {}, 失败: {}, 总计: {}",
delivery.getDeliveryNumber(), successCount, failCount, points.size());
return successCount;
}
private List<YingyanTrackPoint> collectTrackPoints(Delivery delivery, Date startTime, Date endTime) {
// ✅ 修改:从 iot_device_data 表查询设备,而不是 delivery_device 表
QueryWrapper<IotDeviceData> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("delivery_id", delivery.getId());
queryWrapper.and(wrapper -> wrapper.eq("is_delet", 0).or().isNull("is_delet")); // 只查询未删除的设备
List<IotDeviceData> devices = iotDeviceDataMapper.selectList(queryWrapper);
if (CollectionUtils.isEmpty(devices)) {
logger.warn("运单 {} 在 iot_device_data 表中未找到设备deliveryId: {}",
delivery.getDeliveryNumber(), delivery.getId());
return Collections.emptyList();
}
logger.info("运单 {} 从 iot_device_data 表查询到 {} 个设备",
delivery.getDeliveryNumber(), devices.size());
// ✅ 按设备类型分组1=主机2=耳标4=项圈
Map<Integer, List<String>> deviceMap = devices.stream()
.filter(item -> StringUtils.isNotBlank(item.getDeviceId()))
.collect(Collectors.groupingBy(device -> {
Integer deviceType = device.getDeviceType();
// 设备类型1=主机2=耳标4=项圈
return deviceType == null ? 0 : deviceType;
}, Collectors.mapping(IotDeviceData::getDeviceId, Collectors.toList())));
// ✅ 记录设备分组情况
logger.debug("运单 {} 设备分组 - 主机(1): {}, 耳标(2): {}, 项圈(4): {}",
delivery.getDeliveryNumber(),
deviceMap.getOrDefault(1, Collections.emptyList()).size(),
deviceMap.getOrDefault(2, Collections.emptyList()).size(),
deviceMap.getOrDefault(4, Collections.emptyList()).size());
List<YingyanTrackPoint> result = new ArrayList<>();
int remaining = MAX_SYNC_POINTS;
// 1=主机设备,查询 jbq_server_log 表
remaining = appendServerLogs(deviceMap.get(1), startTime, endTime, remaining, result);
if (remaining <= 0) {
return sortPoints(result);
}
// 2=耳标设备,查询 jbq_client_log 表
remaining = appendEarTagLogs(deviceMap.get(2), startTime, endTime, remaining, result);
if (remaining <= 0) {
return sortPoints(result);
}
// 4=项圈设备,查询 xq_client_log 表
appendCollarLogs(deviceMap.get(4), startTime, endTime, remaining, result);
return sortPoints(result);
}
private int appendServerLogs(List<String> deviceIds, Date startTime, Date endTime, int remaining, List<YingyanTrackPoint> result) {
if (CollectionUtils.isEmpty(deviceIds) || remaining <= 0) {
return remaining;
}
List<JbqServerLog> logs = jbqServerLogMapper.listLogsForYingyan(deviceIds, startTime, endTime, remaining);
if (CollectionUtils.isEmpty(logs)) {
return remaining;
}
logs.forEach(log -> {
YingyanTrackPoint point = convertToPoint(log.getLatitude(), log.getLongitude(), log.getUpdateTime());
if (point != null) {
result.add(point);
}
});
return Math.max(0, remaining - logs.size());
}
private int appendEarTagLogs(List<String> deviceIds, Date startTime, Date endTime, int remaining, List<YingyanTrackPoint> result) {
if (CollectionUtils.isEmpty(deviceIds) || remaining <= 0) {
return remaining;
}
List<JbqClientLog> logs = jbqClientLogMapper.listLogsForYingyan(deviceIds, startTime, endTime, remaining);
if (CollectionUtils.isEmpty(logs)) {
return remaining;
}
logs.forEach(log -> {
YingyanTrackPoint point = convertToPoint(log.getLatitude(), log.getLongitude(), log.getUpdateTime());
if (point != null) {
result.add(point);
}
});
return Math.max(0, remaining - logs.size());
}
private void appendCollarLogs(List<String> deviceIds, Date startTime, Date endTime, int remaining, List<YingyanTrackPoint> result) {
if (CollectionUtils.isEmpty(deviceIds) || remaining <= 0) {
return;
}
List<XqClientLog> logs = xqClientLogMapper.listLogsForYingyan(deviceIds, startTime, endTime, remaining);
if (CollectionUtils.isEmpty(logs)) {
return;
}
logs.forEach(log -> {
YingyanTrackPoint point = convertToPoint(log.getLatitude(), log.getLongitude(), log.getUpdateTime());
if (point != null) {
result.add(point);
}
});
}
private List<YingyanTrackPoint> sortPoints(List<YingyanTrackPoint> points) {
if (CollectionUtils.isEmpty(points)) {
return Collections.emptyList();
}
return points.stream()
.sorted(Comparator.comparingLong(YingyanTrackPoint::getLocTime))
.collect(Collectors.toList());
}
private YingyanTrackPoint convertToPoint(String latStr, String lonStr, Date time) {
if (StringUtils.isAnyBlank(latStr, lonStr) || time == null) {
logger.debug("转换轨迹点失败:经纬度或时间为空 - lat={}, lon={}, time={}", latStr, lonStr, time);
return null;
}
try {
double lat = Double.parseDouble(latStr);
double lon = Double.parseDouble(lonStr);
// ✅ 详细验证坐标有效性
if (lat == 0 && lon == 0) {
logger.debug("转换轨迹点失败:经纬度为 (0,0) - lat={}, lon={}", latStr, lonStr);
return null;
}
if (!isValidCoordinate(lat, lon)) {
logger.debug("转换轨迹点失败:经纬度超出有效范围 - lat={}, lon={}", lat, lon);
return null;
}
YingyanTrackPoint point = new YingyanTrackPoint();
point.setLatitude(lat);
point.setLongitude(lon);
point.setLocTime(time.getTime() / 1000);
logger.debug("转换轨迹点成功 - lat={}, lon={}, time={}", lat, lon, time);
return point;
} catch (NumberFormatException ex) {
logger.warn("解析经纬度失败: lat={}, lon={}, error={}", latStr, lonStr, ex.getMessage());
return null;
}
}
private boolean isValidCoordinate(double lat, double lon) {
return lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180;
}
private String resolveEntityName(Delivery delivery) {
if (StringUtils.isNotBlank(delivery.getYingyanEntityName())) {
return delivery.getYingyanEntityName().trim();
}
if (StringUtils.isNotBlank(delivery.getLicensePlate())) {
return delivery.getLicensePlate().trim();
}
return null;
}
private Date determineSyncStart(Delivery delivery) {
if (delivery.getYingyanLastSyncTime() != null) {
return delivery.getYingyanLastSyncTime();
}
if (delivery.getEstimatedDeliveryTime() != null) {
return delivery.getEstimatedDeliveryTime();
}
if (delivery.getEstimatedDepartureTime() != null) {
return delivery.getEstimatedDepartureTime();
}
if (delivery.getCreateTime() != null) {
return delivery.getCreateTime();
}
return new Date(System.currentTimeMillis() - Duration.ofHours(12).toMillis());
}
private void updateLastSync(Delivery delivery, Date newTime) {
if (newTime == null) {
return;
}
Delivery update = new Delivery();
update.setId(delivery.getId());
update.setYingyanLastSyncTime(newTime);
deliveryService.updateById(update);
delivery.setYingyanLastSyncTime(newTime);
}
private void handleArrivalIfNeeded(Delivery delivery, YingyanTrackPoint point) {
if (delivery.getStatus() == null || delivery.getStatus() != 2) {
return;
}
if (StringUtils.isAnyBlank(delivery.getEndLat(), delivery.getEndLon())) {
return;
}
try {
double targetLat = Double.parseDouble(delivery.getEndLat());
double targetLon = Double.parseDouble(delivery.getEndLon());
double distance = calculateDistance(targetLat, targetLon, point.getLatitude(), point.getLongitude());
if (distance <= ARRIVAL_RADIUS_METERS) {
Delivery update = new Delivery();
update.setId(delivery.getId());
update.setStatus(3);
Date arrivalTime = new Date(point.getLocTime() * 1000L);
update.setArrivalTime(arrivalTime);
update.setYingyanLastSyncTime(arrivalTime);
deliveryService.updateById(update);
delivery.setStatus(3);
delivery.setArrivalTime(arrivalTime);
delivery.setYingyanLastSyncTime(arrivalTime);
logger.info("运单 {} 已到达终点,自动更新为已结束,距离 {} 米", delivery.getDeliveryNumber(), Math.round(distance));
}
} catch (NumberFormatException ex) {
logger.warn("运单 {} 终点经纬度格式错误: lat={}, lon={}",
delivery.getDeliveryNumber(), delivery.getEndLat(), delivery.getEndLon());
}
}
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
final double R = 6378137D;
double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lon2 - lon1);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
}

View File

@@ -48,4 +48,9 @@ public interface IDeliveryService extends IService<Delivery> {
AjaxResult detail(Integer id);
PageResultResponse<Delivery> pageQueryListLog(DeliverListDto dto);
/**
* 查询百度鹰眼轨迹与停留点
*/
AjaxResult queryYingyanTrack(Integer deliveryId);
}

View File

@@ -5,6 +5,7 @@ import com.aiotagro.common.core.web.domain.AjaxResult;
import com.aiotagro.common.core.web.domain.PageResultResponse;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import java.util.Map;
/**
@@ -54,5 +55,13 @@ public interface IOrderService extends IService<Order> {
* @return AjaxResult
*/
AjaxResult getOrderDetail(Integer id);
/**
* 批量导入订单
*
* @param orders 订单列表
* @return AjaxResult
*/
AjaxResult batchImportOrders(List<Map<String, Object>> orders);
}

View File

@@ -50,17 +50,32 @@ public class IotDeviceLogSyncService {
@Transactional
public void syncDeviceDataToLogs() {
try {
logger.info("开始执行设备日志同步任务");
logger.info("========== 开始执行设备日志同步任务 ==========");
// 查询所有设备数据
List<IotDeviceData> allDevices = iotDeviceDataMapper.selectList(null);
// 查询绑定了运送清单的设备数据delivery_id 不为空)
QueryWrapper<IotDeviceData> queryWrapper = new QueryWrapper<>();
queryWrapper.isNotNull("delivery_id");
List<IotDeviceData> allDevices = iotDeviceDataMapper.selectList(queryWrapper);
if (allDevices.isEmpty()) {
logger.warn("未找到任何设备数据");
logger.warn("未找到任何绑定了运送清单的设备数据");
return;
}
logger.info("找到 {} 个设备,开始同步到日志表", allDevices.size());
logger.info("找到 {} 个绑定了运送清单的设备,开始同步到日志表", allDevices.size());
// ✅ 统计有经纬度数据的设备数量
long devicesWithCoordinates = allDevices.stream()
.filter(device -> device.getLatitude() != null && device.getLongitude() != null)
.filter(device -> {
String lat = sanitizeCoordinate(device.getLatitude());
String lng = sanitizeCoordinate(device.getLongitude());
return lat != null && lng != null && !lat.equals("0") && !lng.equals("0");
})
.count();
logger.info("其中 {} 个设备包含有效的经纬度坐标数据", devicesWithCoordinates);
logger.info("注意:设备日志同步任务仅将数据同步到日志表,不直接上传到百度鹰眼服务");
logger.info("百度鹰眼轨迹上传由 DeliveryYingyanSyncService 定时任务负责");
int hostCount = 0;
int earTagCount = 0;
@@ -72,6 +87,7 @@ public class IotDeviceLogSyncService {
List<XqClientLog> collarLogs = new ArrayList<>();
// 遍历所有设备,根据设备类型分组
int devicesWithCoordinatesCount = 0;
for (IotDeviceData device : allDevices) {
try {
Integer deviceType = device.getDeviceType();
@@ -80,6 +96,23 @@ public class IotDeviceLogSyncService {
logger.warn("设备 {} 的设备类型为空,跳过", device.getDeviceId());
continue;
}
// ✅ 记录设备经纬度信息
boolean hasValidCoordinates = false;
if (device.getLatitude() != null && device.getLongitude() != null) {
String lat = sanitizeCoordinate(device.getLatitude());
String lng = sanitizeCoordinate(device.getLongitude());
if (lat != null && lng != null && !lat.equals("0") && !lng.equals("0")) {
hasValidCoordinates = true;
devicesWithCoordinatesCount++;
logger.debug("设备 {} (类型: {}, 运单ID: {}) 包含经纬度坐标 - 纬度: {}, 经度: {}",
device.getDeviceId(), deviceType, device.getDeliveryId(), lat, lng);
}
}
if (!hasValidCoordinates) {
logger.debug("设备 {} (类型: {}, 运单ID: {}) 无有效经纬度坐标",
device.getDeviceId(), deviceType, device.getDeliveryId());
}
switch (deviceType) {
case 1: // 主机设备
@@ -167,6 +200,9 @@ public class IotDeviceLogSyncService {
}
logger.info("设备日志同步完成 - 主机: {}, 耳标: {}, 项圈: {}", hostCount, earTagCount, collarCount);
logger.info("包含有效经纬度坐标的设备数量: {}", devicesWithCoordinatesCount);
logger.info("========== 设备日志同步任务执行完成 ==========");
logger.info("提示:同步到日志表的经纬度数据将由 DeliveryYingyanSyncService 定时任务上传到百度鹰眼服务");
} catch (Exception e) {
logger.error("设备日志同步任务执行失败", e);

View File

@@ -5,8 +5,11 @@ import com.aiotagro.cattletrade.business.dto.DeliveryAddDto;
import com.aiotagro.cattletrade.business.dto.DeliveryCreateDto;
import com.aiotagro.cattletrade.business.dto.DeliveryEditDto;
import com.aiotagro.cattletrade.business.dto.DeliveryQueryDto;
import com.aiotagro.cattletrade.business.dto.yingyan.YingyanStayPoint;
import com.aiotagro.cattletrade.business.dto.yingyan.YingyanTrackPoint;
import com.aiotagro.cattletrade.business.entity.*;
import com.aiotagro.cattletrade.business.mapper.*;
import com.aiotagro.cattletrade.business.service.BaiduYingyanService;
import com.aiotagro.cattletrade.business.service.IDeliveryService;
import com.aiotagro.cattletrade.business.service.IDeliveryDeviceService;
import com.aiotagro.cattletrade.business.service.IXqClientService;
@@ -19,6 +22,7 @@ import com.aiotagro.common.core.utils.StringUtils;
import com.aiotagro.common.core.web.domain.AjaxResult;
import com.aiotagro.common.core.web.domain.PageResultResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
@@ -64,6 +68,9 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
private MemberDriverMapper memberDriverMapper;
@Autowired
private IDeliveryDeviceService deliveryDeviceService;
@Autowired
private BaiduYingyanService baiduYingyanService;
@Autowired
private IXqClientService xqClientService;
@Autowired
@@ -84,7 +91,6 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
// 调试:打印接收到的所有参数
Page<Delivery> result = PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
LambdaQueryWrapper<Delivery> wrapper = new LambdaQueryWrapper<>();
// 运输单号模糊查询
@@ -119,12 +125,20 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
}
wrapper.orderByDesc(Delivery::getId);
List<Delivery> list = this.list(wrapper);
if(CollectionUtils.isNotEmpty(list)){
}
if(CollectionUtils.isNotEmpty(list)){
list.forEach(delivery -> {
// 判断是否需要数据权限过滤
boolean needPermissionFilter = !SecurityUtil.isSuperAdmin() && StringUtils.isNotEmpty(currentUserMobile);
List<Delivery> list;
long total;
if (needPermissionFilter) {
// 需要权限过滤:先查询所有数据,填充信息,过滤后再分页
list = this.list(wrapper);
// 填充关联信息(供应商、资金方、采购商、司机等)
if(CollectionUtils.isNotEmpty(list)){
list.forEach(delivery -> {
if(userId.equals(delivery.getCheckBy())){
//判断是否需要核验1需要核验2不需要核验
delivery.setIfCheck(1);
@@ -344,58 +358,283 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
}
}
});
}
// 数据权限过滤:非超级管理员只能看到与自己手机号相关的订单
if (!SecurityUtil.isSuperAdmin() && StringUtils.isNotEmpty(currentUserMobile)) {
list = list.stream().filter(delivery -> {
boolean hasPermission = false;
list = list.stream().filter(delivery -> {
boolean hasPermission = false;
// 检查是否是司机
if (StringUtils.isNotEmpty(delivery.getDriverMobile()) &&
currentUserMobile.equals(delivery.getDriverMobile())) {
hasPermission = true;
}
// 检查是否是供应商(可能有多个供应商)
if (!hasPermission && StringUtils.isNotEmpty(delivery.getSupplierMobile())) {
String[] supplierMobiles = delivery.getSupplierMobile().split(",");
for (String mobile : supplierMobiles) {
if (currentUserMobile.equals(mobile.trim())) {
hasPermission = true;
break;
}
// 检查是否是司机
if (StringUtils.isNotEmpty(delivery.getDriverMobile()) &&
currentUserMobile.equals(delivery.getDriverMobile())) {
hasPermission = true;
}
// 检查是否是供应商(可能有多个供应商)
if (!hasPermission && StringUtils.isNotEmpty(delivery.getSupplierMobile())) {
String[] supplierMobiles = delivery.getSupplierMobile().split(",");
for (String mobile : supplierMobiles) {
if (currentUserMobile.equals(mobile.trim())) {
hasPermission = true;
break;
}
}
// 检查是否是资金方
if (!hasPermission && StringUtils.isNotEmpty(delivery.getFundMobile()) &&
currentUserMobile.equals(delivery.getFundMobile())) {
hasPermission = true;
}
// 检查是否是采购商
if (!hasPermission && StringUtils.isNotEmpty(delivery.getBuyerMobile()) &&
currentUserMobile.equals(delivery.getBuyerMobile())) {
hasPermission = true;
}
if (!hasPermission) {
}
return hasPermission;
}).collect(Collectors.toList());
}
} else if (SecurityUtil.isSuperAdmin()) {
// 检查是否是资金方
if (!hasPermission && StringUtils.isNotEmpty(delivery.getFundMobile()) &&
currentUserMobile.equals(delivery.getFundMobile())) {
hasPermission = true;
}
// 检查是否是采购商
if (!hasPermission && StringUtils.isNotEmpty(delivery.getBuyerMobile()) &&
currentUserMobile.equals(delivery.getBuyerMobile())) {
hasPermission = true;
}
return hasPermission;
}).collect(Collectors.toList());
// 获取过滤后的总数
total = list.size();
// 手动分页
int startIndex = (dto.getPageNum() - 1) * dto.getPageSize();
int endIndex = Math.min(startIndex + dto.getPageSize(), list.size());
if (startIndex < list.size()) {
list = list.subList(startIndex, endIndex);
} else {
list = new ArrayList<>();
}
} else {
// 不需要权限过滤直接使用PageHelper分页
Page<Delivery> result = PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
list = this.list(wrapper);
total = result.getTotal();
// 填充关联信息(供应商、资金方、采购商、司机等)
if(CollectionUtils.isNotEmpty(list)){
list.forEach(delivery -> {
if(userId.equals(delivery.getCheckBy())){
//判断是否需要核验1需要核验2不需要核验
delivery.setIfCheck(1);
}
// 查询四个角色的手机号(供应商、资金方、采购商、司机)
try {
// 1. 查询供应商信息supplierId是逗号分隔的字符串
if (StringUtils.isNotEmpty(delivery.getSupplierId())) {
String[] supplierIds = delivery.getSupplierId().split(",");
List<String> supplierNames = new ArrayList<>();
List<String> supplierMobiles = new ArrayList<>();
for (String supplierId : supplierIds) {
if (StringUtils.isNotEmpty(supplierId.trim())) {
try {
Integer sid = Integer.parseInt(supplierId.trim());
// 查询member和member_user表关联数据
Map<String, Object> supplierInfo = memberMapper.selectMemberUserById(sid);
if (supplierInfo != null) {
String username = (String) supplierInfo.get("username");
String mobile = (String) supplierInfo.get("mobile");
if (StringUtils.isNotEmpty(username)) {
supplierNames.add(username);
}
if (StringUtils.isNotEmpty(mobile)) {
supplierMobiles.add(mobile);
}
}
} catch (NumberFormatException e) {
}
}
}
if (!supplierNames.isEmpty()) {
delivery.setSupplierName(String.join(",", supplierNames));
} else if (!supplierMobiles.isEmpty()) {
// 如果用户名为空,使用手机号作为备选
delivery.setSupplierName(String.join(",", supplierMobiles));
}
if (!supplierMobiles.isEmpty()) {
delivery.setSupplierMobile(String.join(",", supplierMobiles));
}
}
// 2. 查询资金方信息
if (delivery.getFundId() != null) {
Map<String, Object> fundInfo = memberMapper.selectMemberUserById(delivery.getFundId());
if (fundInfo != null) {
String username = (String) fundInfo.get("username");
String mobile = (String) fundInfo.get("mobile");
if (StringUtils.isNotEmpty(username)) {
delivery.setFundName(username);
} else if (StringUtils.isNotEmpty(mobile)) {
// 如果用户名为空,使用手机号作为备选
delivery.setFundName(mobile);
}
if (StringUtils.isNotEmpty(mobile)) {
delivery.setFundMobile(mobile);
}
}
}
// 3. 查询采购商信息
if (delivery.getBuyerId() != null) {
Map<String, Object> buyerInfo = memberMapper.selectMemberUserById(delivery.getBuyerId());
if (buyerInfo != null) {
String username = (String) buyerInfo.get("username");
String mobile = (String) buyerInfo.get("mobile");
if (StringUtils.isNotEmpty(username)) {
delivery.setBuyerName(username);
} else if (StringUtils.isNotEmpty(mobile)) {
// 如果用户名为空,使用手机号作为备选
delivery.setBuyerName(mobile);
}
if (StringUtils.isNotEmpty(mobile)) {
delivery.setBuyerMobile(mobile);
}
}
}
// 4. 查询司机手机号如果有司机ID
if (delivery.getDriverId() != null) {
try {
Map<String, Object> driverInfo = memberDriverMapper.selectDriverById(delivery.getDriverId());
if (driverInfo != null) {
String driverName = (String) driverInfo.get("username");
String driverMobile = (String) driverInfo.get("mobile");
String carImg = (String) driverInfo.get("car_img");
if (StringUtils.isNotEmpty(driverMobile)) {
delivery.setDriverMobile(driverMobile);
}
if (StringUtils.isNotEmpty(driverName)) {
delivery.setDriverName(driverName);
}
// 优先从车辆表获取车身照片(根据车牌号)
// 如果车辆表中没有,再从司机信息中获取作为后备
boolean vehiclePhotoSet = false;
if (delivery.getLicensePlate() != null && StringUtils.isNotEmpty(delivery.getLicensePlate())) {
try {
Vehicle vehicle = vehicleMapper.selectByLicensePlate(delivery.getLicensePlate());
if (vehicle != null) {
String carFrontPhoto = vehicle.getCarFrontPhoto();
String carRearPhoto = vehicle.getCarRearPhoto();
if (StringUtils.isNotEmpty(carFrontPhoto) || StringUtils.isNotEmpty(carRearPhoto)) {
delivery.setCarFrontPhoto(StringUtils.isNotEmpty(carFrontPhoto) ? carFrontPhoto : null);
delivery.setCarBehindPhoto(StringUtils.isNotEmpty(carRearPhoto) ? carRearPhoto : null);
vehiclePhotoSet = true;
}
}
} catch (Exception e) {
logger.error("从车辆表获取照片失败: " + e.getMessage(), e);
}
}
// 如果车辆表中没有照片,从司机信息中获取作为后备
if (!vehiclePhotoSet && carImg != null && !carImg.isEmpty()) {
// 按逗号分割car_img字段分别映射到车头和车尾照片
String[] carImgUrls = carImg.split(",");
if (carImgUrls.length >= 2) {
// 逗号前面的URL作为车尾照片
String carBehindPhoto = carImgUrls[0].trim();
// 逗号后面的URL作为车头照片
String carFrontPhoto = carImgUrls[1].trim();
delivery.setCarBehindPhoto(carBehindPhoto);
delivery.setCarFrontPhoto(carFrontPhoto);
} else if (carImgUrls.length == 1) {
// 只有一个URL时同时设置为车头和车尾照片
String singlePhoto = carImgUrls[0].trim();
delivery.setCarFrontPhoto(singlePhoto);
delivery.setCarBehindPhoto(singlePhoto);
} else {
// 没有有效URL设置为null
delivery.setCarFrontPhoto(null);
delivery.setCarBehindPhoto(null);
}
} else if (!vehiclePhotoSet) {
// 如果车辆表和司机信息中都没有照片设置为null
delivery.setCarFrontPhoto(null);
delivery.setCarBehindPhoto(null);
}
}
} catch (Exception e) {
}
}
} catch (Exception e) {
e.printStackTrace();
}
// 统计登记设备数量(耳标+项圈)
Integer currentDeliveryId = delivery.getId();
if (currentDeliveryId != null) {
try {
// 统计耳标设备数量
LambdaQueryWrapper<DeliveryDevice> earTagWrapper = new LambdaQueryWrapper<>();
earTagWrapper.eq(DeliveryDevice::getDeliveryId, currentDeliveryId)
.eq(DeliveryDevice::getDeviceType, 2);
long earTagCount = deliveryDeviceService.count(earTagWrapper);
// 统计项圈设备数量
LambdaQueryWrapper<DeliveryDevice> collarWrapper = new LambdaQueryWrapper<>();
collarWrapper.eq(DeliveryDevice::getDeliveryId, currentDeliveryId)
.eq(DeliveryDevice::getDeviceType, 3);
long collarCount = deliveryDeviceService.count(collarWrapper);
// 设置总设备数量和耳标数量
int totalDeviceCount = (int) (earTagCount + collarCount);
delivery.setRegisteredJbqCount(totalDeviceCount);
delivery.setEarTagCount((int) earTagCount);
// 设置已分配设备数量,与登记设备数量保持一致
delivery.setBindJbqCount(totalDeviceCount);
// 统计已佩戴设备数量bandge_status = 1
int wornDeviceCount = 0;
try {
// 统计已佩戴的耳标设备数量通过delivery_device表关联
LambdaQueryWrapper<DeliveryDevice> wornEarTagWrapper = new LambdaQueryWrapper<>();
wornEarTagWrapper.eq(DeliveryDevice::getDeliveryId, currentDeliveryId)
.eq(DeliveryDevice::getDeviceType, 2)
.eq(DeliveryDevice::getIsWare, 1); // 1表示已佩戴
long wornEarTagCount = deliveryDeviceService.count(wornEarTagWrapper);
// 统计已佩戴的项圈设备数量通过delivery_device表关联xq_client表
LambdaQueryWrapper<DeliveryDevice> wornCollarWrapper = new LambdaQueryWrapper<>();
wornCollarWrapper.eq(DeliveryDevice::getDeliveryId, currentDeliveryId)
.eq(DeliveryDevice::getDeviceType, 3);
List<DeliveryDevice> collarDevices = deliveryDeviceService.list(wornCollarWrapper);
int wornCollarCount = 0;
for (DeliveryDevice device : collarDevices) {
// 查询xq_client表中的bandge_status
LambdaQueryWrapper<XqClient> xqWrapper = new LambdaQueryWrapper<>();
xqWrapper.eq(XqClient::getSn, device.getDeviceId());
XqClient xqClient = xqClientService.getOne(xqWrapper);
if (xqClient != null && xqClient.getBandgeStatus() != null && xqClient.getBandgeStatus() == 1) {
wornCollarCount++;
}
}
wornDeviceCount = (int) (wornEarTagCount + wornCollarCount);
delivery.setWareCount(wornDeviceCount);
} catch (Exception e) {
delivery.setWareCount(0);
}
} catch (Exception e) {
delivery.setRegisteredJbqCount(0);
delivery.setBindJbqCount(0);
delivery.setWareCount(0);
}
}
});
}
}
// 更新分页信息
long filteredTotal = list.size();
return new PageResultResponse(filteredTotal, list);
// 返回分页结果
return new PageResultResponse<>(total, list);
}
/**
@@ -754,6 +993,44 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
if (dto.getDriverMobile() != null) {
delivery.setDriverMobile(dto.getDriverMobile());
}
if (dto.getLicensePlate() != null) {
String oldLicensePlate = delivery.getLicensePlate();
String newLicensePlate = StringUtils.isNotEmpty(dto.getLicensePlate()) ? dto.getLicensePlate().trim() : null;
delivery.setLicensePlate(newLicensePlate);
logger.info("更新车牌号: oldLicensePlate={}, newLicensePlate={}", oldLicensePlate, newLicensePlate);
// 如果车牌号发生变化,且运单状态为运输中(2),需要同步更新鹰眼终端名称
if (StringUtils.isNotEmpty(newLicensePlate) &&
delivery.getStatus() != null &&
delivery.getStatus() == 2) {
// 检查车牌号是否真的发生了变化
boolean licensePlateChanged = oldLicensePlate == null || !newLicensePlate.equals(oldLicensePlate);
if (licensePlateChanged) {
String newEntityName = newLicensePlate;
String oldEntityName = delivery.getYingyanEntityName();
// 更新鹰眼终端名称
delivery.setYingyanEntityName(newEntityName);
// 如果旧的终端名称存在且与新名称不同,需要重新创建终端
if (StringUtils.isNotEmpty(oldEntityName) && !oldEntityName.equals(newEntityName)) {
logger.info("车牌号变更,重新创建鹰眼终端: oldEntityName={}, newEntityName={}", oldEntityName, newEntityName);
boolean ensureResult = baiduYingyanService.ensureEntity(newEntityName);
if (!ensureResult) {
logger.warn("运单 {} 重新创建百度鹰眼终端失败,将在后台重试", delivery.getDeliveryNumber());
}
} else {
// 如果之前没有终端名称,直接创建
boolean ensureResult = baiduYingyanService.ensureEntity(newEntityName);
if (!ensureResult) {
logger.warn("运单 {} 创建百度鹰眼终端失败,将在后台重试", delivery.getDeliveryNumber());
}
}
}
}
}
if (dto.getBuyerId() != null) {
delivery.setBuyerId(dto.getBuyerId());
}
@@ -1277,6 +1554,11 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
public PageResultResponse<DeliveryLogVo> pageQueryList(DeliverListDto dto) {
//获取当前登录人的信息
String currentUserMobile = SecurityUtil.getUserMobile();
boolean isSuperAdmin = SecurityUtil.isSuperAdmin();
logger.info("[预警列表查询] 开始查询,参数: pageNum={}, pageSize={}, deliveryNumber={}, licensePlate={}, warningType={}, startTime={}, endTime={}, isSuperAdmin={}",
dto.getPageNum(), dto.getPageSize(), dto.getDeliveryNumber(), dto.getLicensePlate(),
dto.getWarningType(), dto.getStartTime(), dto.getEndTime(), isSuperAdmin);
Page<Delivery> result = PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
if(StringUtils.isNotEmpty(dto.getStartTime())){
@@ -1287,16 +1569,74 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
String endTime = dto.getEndTime() + " 23:59:59";
dto.setEndTime(endTime);
}
List<Delivery> resList = this.baseMapper.getPageWarningLog(dto);
long totalBeforeFilter = result.getTotal(); // 保存过滤前的总数
logger.info("[预警列表查询] SQL查询结果: 总数={}, 当前页数据量={}", totalBeforeFilter, resList.size());
// ✅ 调试:如果查询结果为空,检查可能的原因
if (totalBeforeFilter == 0 && resList.isEmpty()) {
logger.warn("[预警列表查询] ⚠️ 查询结果为空,可能原因:");
logger.warn("[预警列表查询] 1. delivery 表和 warning_log 表的 delivery_id 不匹配");
logger.warn("[预警列表查询] 2. warning_log 表中的 delivery_id 在 delivery 表中不存在");
logger.warn("[预警列表查询] 3. SQL 查询逻辑有问题");
// 检查 warning_log 表中是否有数据
try {
long warningLogCount = warningLogMapper.selectCount(null);
logger.info("[预警列表查询] warning_log 表总记录数: {}", warningLogCount);
// 检查有 delivery_id 的记录数
QueryWrapper<WarningLog> countWrapper = new QueryWrapper<>();
countWrapper.in("warning_type", 2,3,4,5,6,7,8,9);
long validWarningCount = warningLogMapper.selectCount(countWrapper);
logger.info("[预警列表查询] warning_log 表中预警类型在(2-9)范围内的记录数: {}", validWarningCount);
// 检查 delivery 表中有多少运单
long deliveryCount = this.count();
logger.info("[预警列表查询] delivery 表总记录数: {}", deliveryCount);
// ✅ 关键调试:检查 warning_log 表中的 delivery_id 是否在 delivery 表中存在
QueryWrapper<WarningLog> deliveryIdWrapper = new QueryWrapper<>();
deliveryIdWrapper.in("warning_type", 2,3,4,5,6,7,8,9);
deliveryIdWrapper.select("DISTINCT delivery_id");
List<WarningLog> warningLogsWithDeliveryId = warningLogMapper.selectList(deliveryIdWrapper);
Set<Integer> warningLogDeliveryIds = warningLogsWithDeliveryId.stream()
.map(WarningLog::getDeliveryId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
logger.info("[预警列表查询] warning_log 表中不重复的 delivery_id 数量: {}", warningLogDeliveryIds.size());
// 检查这些 delivery_id 在 delivery 表中是否存在
if (!warningLogDeliveryIds.isEmpty()) {
List<Integer> deliveryIds = this.listByIds(new ArrayList<>(warningLogDeliveryIds))
.stream()
.map(Delivery::getId)
.collect(Collectors.toList());
logger.info("[预警列表查询] 在 delivery 表中存在的 delivery_id 数量: {}", deliveryIds.size());
logger.info("[预警列表查询] 在 delivery 表中存在的 delivery_id: {}", deliveryIds);
// 找出不匹配的 delivery_id
Set<Integer> missingDeliveryIds = new HashSet<>(warningLogDeliveryIds);
missingDeliveryIds.removeAll(deliveryIds);
if (!missingDeliveryIds.isEmpty()) {
logger.warn("[预警列表查询] ⚠️ warning_log 表中有 {} 个 delivery_id 在 delivery 表中不存在", missingDeliveryIds.size());
logger.warn("[预警列表查询] 不匹配的 delivery_id 示例前10个: {}",
missingDeliveryIds.stream().limit(10).collect(Collectors.toList()));
}
}
} catch (Exception e) {
logger.error("[预警列表查询] 调试查询失败: {}", e.getMessage(), e);
}
}
// 数据权限过滤:非超级管理员只能看到与自己手机号相关的订单
if (!SecurityUtil.isSuperAdmin() && StringUtils.isNotEmpty(currentUserMobile)) {
if (!isSuperAdmin && StringUtils.isNotEmpty(currentUserMobile)) {
int beforeFilterSize = resList.size();
resList = resList.stream().filter(delivery -> {
boolean hasPermission = false;
// 检查是否是司机
if (StringUtils.isNotEmpty(delivery.getDriverMobile()) &&
currentUserMobile.equals(delivery.getDriverMobile())) {
@@ -1326,26 +1666,61 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
hasPermission = true;
}
if (!hasPermission) {
}
return hasPermission;
}).collect(Collectors.toList());
} else if (SecurityUtil.isSuperAdmin()) {
logger.info("[预警列表查询] 权限过滤后: 过滤前={}, 过滤后={}", beforeFilterSize, resList.size());
} else if (isSuperAdmin) {
logger.info("[预警列表查询] 超级管理员,跳过权限过滤");
} else {
logger.warn("[预警列表查询] 未获取到用户手机号,跳过权限过滤");
}
resList.forEach(deliveryLogVo -> {
String warningType = deliveryLogVo.getWarningType();
// ✅ 修复:将 Delivery 实体转换为 DeliveryLogVo
List<DeliveryLogVo> voList = new ArrayList<>();
for (Delivery delivery : resList) {
DeliveryLogVo vo = new DeliveryLogVo();
vo.setId(delivery.getId());
vo.setDeliveryId(delivery.getId());
vo.setDeliveryNumber(delivery.getDeliveryNumber());
vo.setLicensePlate(delivery.getLicensePlate());
vo.setStatus(delivery.getStatus());
vo.setCarFrontPhoto(delivery.getCarFrontPhoto());
vo.setRegisteredJbqCount(delivery.getRegisteredJbqCount());
vo.setInventoryJbqCount(delivery.getInventoryJbqCount());
vo.setWarningType(delivery.getWarningType());
vo.setWarningTime(delivery.getWarningTime());
vo.setCreateByDesc(delivery.getCreateByName());
// ✅ 新增:填充起始地、目的地、创建时间、预计送达时间、司机姓名、创建人
vo.setStartLocation(delivery.getStartLocation());
vo.setEndLocation(delivery.getEndLocation());
vo.setCreateTime(delivery.getCreateTime());
vo.setEstimatedDeliveryTime(delivery.getEstimatedDeliveryTime());
vo.setDriverName(delivery.getDriverName());
vo.setCreateByName(delivery.getCreateByName());
// 设置预警类型描述
String warningType = delivery.getWarningType();
if(StringUtils.isNotEmpty(warningType)){
deliveryLogVo.setWarningTypeDesc(EnumUtil.getEnumConstant(WarningStatusAdminEnum.class , Integer.parseInt(warningType)).getDescription());
try {
vo.setWarningTypeDesc(EnumUtil.getEnumConstant(WarningStatusAdminEnum.class , Integer.parseInt(warningType)).getDescription());
} catch (Exception e) {
logger.warn("[预警列表查询] 预警类型解析失败: {}", warningType);
vo.setWarningTypeDesc("未知类型");
}
}
});
voList.add(vo);
}
// 更新分页信息
long filteredTotal = resList.size();
return new PageResultResponse(filteredTotal, resList);
// ✅ 修复:使用分页查询的总数,而不是过滤后的数量
// 如果是超级管理员,使用 SQL 查询的总数;否则使用过滤后的数量
long finalTotal = isSuperAdmin ? totalBeforeFilter : voList.size();
logger.info("[预警列表查询] 最终返回: 总数={}, 数据量={}", finalTotal, voList.size());
return new PageResultResponse<DeliveryLogVo>(finalTotal, voList);
}
@Override
@@ -1654,6 +2029,107 @@ public class DeliveryServiceImpl extends ServiceImpl<DeliveryMapper, Delivery> i
return AjaxResult.success(resMap);
}
@Override
public AjaxResult queryYingyanTrack(Integer deliveryId) {
if (deliveryId == null) {
return AjaxResult.error("运单ID不能为空");
}
Delivery delivery = this.getById(deliveryId);
if (delivery == null) {
return AjaxResult.error("运单不存在");
}
if (delivery.getStatus() == null || delivery.getStatus() == 1) {
return AjaxResult.error("该运单尚未开始运输");
}
String entityName = StringUtils.defaultIfBlank(delivery.getYingyanEntityName(), delivery.getLicensePlate());
if (StringUtils.isBlank(entityName)) {
return AjaxResult.error("缺少车牌信息,无法查询轨迹");
}
// 去除空格,确保格式一致
entityName = entityName.trim();
baiduYingyanService.ensureEntity(entityName);
// ✅ 按照用户要求使用预计开始时间estimatedDeliveryTime作为轨迹查询的起始时间
// 如果没有 estimatedDeliveryTime则依次尝试 estimatedDepartureTime、createTime
long startTime = dateToSeconds(delivery.getEstimatedDeliveryTime());
if (startTime <= 0) {
startTime = dateToSeconds(delivery.getEstimatedDepartureTime());
}
if (startTime <= 0) {
startTime = dateToSeconds(delivery.getCreateTime());
}
if (startTime <= 0) {
startTime = System.currentTimeMillis() / 1000 - 12 * 3600;
}
logger.info("运单 {} 轨迹查询开始时间确定 - estimatedDeliveryTime: {}, estimatedDepartureTime: {}, createTime: {}, 最终使用: {}",
delivery.getDeliveryNumber(),
delivery.getEstimatedDeliveryTime(),
delivery.getEstimatedDepartureTime(),
delivery.getCreateTime(),
new Date(startTime * 1000));
long endTime = determineTrackEndTime(delivery);
// ✅ 使用分段查询方法支持超过24小时的轨迹查询
logger.info("查询运单 {} 的百度鹰眼轨迹 - entity={}, startTime={}, endTime={}, 时间跨度={}小时",
delivery.getDeliveryNumber(), entityName, startTime, endTime, (endTime - startTime) / 3600);
// ✅ 确保终端存在后再查询(重要:如果终端不存在,查询会失败)
boolean entityExists = baiduYingyanService.ensureEntity(entityName);
if (!entityExists) {
logger.error("运单 {} 终端不存在且创建失败,无法查询轨迹 - entity={}, deliveryNumber={}, deliveryId={}",
delivery.getDeliveryNumber(), entityName, delivery.getDeliveryNumber(), delivery.getId());
// 返回空结果,而不是继续查询(因为查询肯定会失败)
Map<String, Object> data = new HashMap<>();
data.put("trackPoints", Collections.emptyList());
data.put("stayPoints", Collections.emptyList());
data.put("entityName", entityName);
data.put("startTime", startTime * 1000);
data.put("endTime", endTime * 1000);
data.put("status", delivery.getStatus());
data.put("error", "终端不存在且创建失败,请检查百度鹰眼服务配置或终端名称是否正确");
return AjaxResult.success(data);
}
logger.debug("运单 {} 终端已确保存在,开始查询轨迹 - entity={}",
delivery.getDeliveryNumber(), entityName);
List<YingyanTrackPoint> trackPoints = baiduYingyanService.queryTrackSegmented(entityName, startTime, endTime);
List<YingyanStayPoint> stayPoints = baiduYingyanService.queryStayPointsSegmented(entityName, startTime, endTime, 900);
logger.info("运单 {} 轨迹查询完成 - 轨迹点数: {}, 停留点数: {}",
delivery.getDeliveryNumber(), trackPoints.size(), stayPoints.size());
Map<String, Object> data = new HashMap<>();
data.put("trackPoints", trackPoints);
data.put("stayPoints", stayPoints);
data.put("entityName", entityName);
data.put("startTime", startTime * 1000);
data.put("endTime", endTime * 1000);
data.put("status", delivery.getStatus());
return AjaxResult.success(data);
}
private long determineTrackEndTime(Delivery delivery) {
if (delivery.getStatus() != null && delivery.getStatus() == 3) {
long arrivalTime = dateToSeconds(delivery.getArrivalTime());
if (arrivalTime > 0) {
return arrivalTime;
}
}
return System.currentTimeMillis() / 1000;
}
private long dateToSeconds(Date date) {
if (date == null) {
return 0;
}
return date.getTime() / 1000;
}
/**
* 获取核验状态的中文描述
* @param status 状态码

View File

@@ -1,7 +1,6 @@
package com.aiotagro.cattletrade.business.service.impl;
import com.aiotagro.cattletrade.business.entity.Order;
import com.aiotagro.cattletrade.business.entity.Member;
import com.aiotagro.cattletrade.business.entity.SysUser;
import com.aiotagro.cattletrade.business.mapper.OrderMapper;
import com.aiotagro.cattletrade.business.mapper.MemberMapper;
@@ -20,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@@ -62,9 +62,6 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
logger.info("分页查询订单列表,页码:{},每页数量:{},买方:{},卖方:{},结算方式:{}",
pageNum, pageSize, buyerName, sellerName, settlementType);
// 使用PageHelper进行分页
Page<Order> page = PageHelper.startPage(pageNum, pageSize);
// 构建查询条件
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(settlementType != null, Order::getSettlementType, settlementType);
@@ -89,28 +86,65 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
queryWrapper.orderByDesc(Order::getCreateTime);
// 执行查询
List<Order> list = orderMapper.selectList(queryWrapper);
// 填充关联信息
list.forEach(this::fillOrderInfo);
// 如果提供了买方或卖方名称搜索,进行过滤
List<Order> filteredList = list;
if (buyerName != null && !buyerName.trim().isEmpty()) {
filteredList = filteredList.stream()
.filter(order -> order.getBuyerName() != null && order.getBuyerName().contains(buyerName.trim()))
.collect(java.util.stream.Collectors.toList());
}
if (sellerName != null && !sellerName.trim().isEmpty()) {
filteredList = filteredList.stream()
.filter(order -> order.getSellerName() != null && order.getSellerName().contains(sellerName.trim()))
.collect(java.util.stream.Collectors.toList());
// 判断是否需要先过滤再分页(如果提供了买方或卖方名称搜索)
boolean needFilter = (buyerName != null && !buyerName.trim().isEmpty()) ||
(sellerName != null && !sellerName.trim().isEmpty());
List<Order> filteredList;
long total;
if (needFilter) {
// 需要过滤的情况:先查询所有数据,填充信息,过滤,然后手动分页
List<Order> allList = orderMapper.selectList(queryWrapper);
// 批量填充关联信息(优化性能)
fillOrderInfoBatch(allList);
// 进行过滤
filteredList = allList;
if (buyerName != null && !buyerName.trim().isEmpty()) {
filteredList = filteredList.stream()
.filter(order -> order.getBuyerName() != null && order.getBuyerName().contains(buyerName.trim()))
.collect(java.util.stream.Collectors.toList());
}
if (sellerName != null && !sellerName.trim().isEmpty()) {
filteredList = filteredList.stream()
.filter(order -> order.getSellerName() != null && order.getSellerName().contains(sellerName.trim()))
.collect(java.util.stream.Collectors.toList());
}
// 获取总数
total = filteredList.size();
// 手动分页
int startIndex = (pageNum - 1) * pageSize;
int endIndex = Math.min(startIndex + pageSize, filteredList.size());
if (startIndex < filteredList.size()) {
filteredList = filteredList.subList(startIndex, endIndex);
} else {
filteredList = new ArrayList<>();
}
logger.info("查询到{}条订单记录,过滤后{}条,分页后{}条", allList.size(), total, filteredList.size());
} else {
// 不需要过滤的情况直接使用PageHelper分页
Page<Order> page = PageHelper.startPage(pageNum, pageSize);
// 执行查询
List<Order> list = orderMapper.selectList(queryWrapper);
// 批量填充关联信息(优化性能)
fillOrderInfoBatch(list);
// 获取总数和分页数据
total = page.getTotal();
filteredList = list;
logger.info("查询到{}条订单记录,分页后{}条", total, filteredList.size());
}
// 构建分页结果
logger.info("查询到{}条订单记录,过滤后{}条", list.size(), filteredList.size());
return new PageResultResponse<>(filteredList.size(), filteredList);
return new PageResultResponse<>(total, filteredList);
}
/**
@@ -290,8 +324,178 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
return AjaxResult.success(order);
}
/**
* 批量填充订单关联信息(优化性能,减少数据库查询次数)
*/
private void fillOrderInfoBatch(List<Order> orders) {
if (orders == null || orders.isEmpty()) {
return;
}
// 收集所有需要查询的ID
java.util.Set<Integer> buyerIds = new java.util.HashSet<>();
java.util.Set<Integer> sellerIds = new java.util.HashSet<>();
java.util.Set<Integer> creatorIds = new java.util.HashSet<>();
for (Order order : orders) {
// 收集买方ID
if (order.getBuyerId() != null && !order.getBuyerId().isEmpty()) {
Arrays.stream(order.getBuyerId().split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEach(id -> {
try {
buyerIds.add(Integer.parseInt(id));
} catch (NumberFormatException e) {
logger.warn("无效的买方ID{}", id);
}
});
}
// 收集卖方ID
if (order.getSellerId() != null && !order.getSellerId().isEmpty()) {
Arrays.stream(order.getSellerId().split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEach(id -> {
try {
sellerIds.add(Integer.parseInt(id));
} catch (NumberFormatException e) {
logger.warn("无效的卖方ID{}", id);
}
});
}
// 收集创建人ID
if (order.getCreatedBy() != null) {
creatorIds.add(order.getCreatedBy());
}
}
// 批量查询买方信息
java.util.Map<Integer, String> buyerNameMap = new java.util.HashMap<>();
if (!buyerIds.isEmpty()) {
List<Map<String, Object>> buyerList = memberMapper.selectMemberUserByIds(new ArrayList<>(buyerIds));
for (Map<String, Object> buyer : buyerList) {
Object idObj = buyer.get("id");
Integer id = null;
if (idObj != null) {
if (idObj instanceof Integer) {
id = (Integer) idObj;
} else if (idObj instanceof Long) {
id = ((Long) idObj).intValue();
} else if (idObj instanceof Number) {
id = ((Number) idObj).intValue();
}
}
String username = (String) buyer.get("username");
if (id != null && username != null) {
buyerNameMap.put(id, username);
}
}
}
// 批量查询卖方信息
java.util.Map<Integer, String> sellerNameMap = new java.util.HashMap<>();
if (!sellerIds.isEmpty()) {
List<Map<String, Object>> sellerList = memberMapper.selectMemberUserByIds(new ArrayList<>(sellerIds));
for (Map<String, Object> seller : sellerList) {
Object idObj = seller.get("id");
Integer id = null;
if (idObj != null) {
if (idObj instanceof Integer) {
id = (Integer) idObj;
} else if (idObj instanceof Long) {
id = ((Long) idObj).intValue();
} else if (idObj instanceof Number) {
id = ((Number) idObj).intValue();
}
}
String username = (String) seller.get("username");
if (id != null && username != null) {
sellerNameMap.put(id, username);
}
}
}
// 批量查询创建人信息
java.util.Map<Integer, String> creatorNameMap = new java.util.HashMap<>();
if (!creatorIds.isEmpty()) {
List<SysUser> creatorList = sysUserMapper.selectBatchIds(creatorIds);
for (SysUser user : creatorList) {
if (user != null && user.getId() != null && user.getName() != null) {
creatorNameMap.put(user.getId(), user.getName());
}
}
}
// 批量填充订单信息
for (Order order : orders) {
// 填充买方名称
if (order.getBuyerId() != null && !order.getBuyerId().isEmpty()) {
String buyerNames = Arrays.stream(order.getBuyerId().split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(id -> {
try {
Integer buyerId = Integer.parseInt(id);
return buyerNameMap.getOrDefault(buyerId, "");
} catch (NumberFormatException e) {
return "";
}
})
.filter(name -> !name.isEmpty())
.collect(Collectors.joining(", "));
order.setBuyerName(buyerNames);
}
// 填充卖方名称
if (order.getSellerId() != null && !order.getSellerId().isEmpty()) {
String sellerNames = Arrays.stream(order.getSellerId().split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(id -> {
try {
Integer sellerId = Integer.parseInt(id);
return sellerNameMap.getOrDefault(sellerId, "");
} catch (NumberFormatException e) {
return "";
}
})
.filter(name -> !name.isEmpty())
.collect(Collectors.joining(", "));
order.setSellerName(sellerNames);
}
// 填充创建人名称
if (order.getCreatedBy() != null) {
String creatorName = creatorNameMap.get(order.getCreatedBy());
if (creatorName != null) {
order.setCreatedByName(creatorName);
}
}
// 填充结算方式描述
if (order.getSettlementType() != null) {
switch (order.getSettlementType()) {
case 1:
order.setSettlementTypeDesc("上车重量");
break;
case 2:
order.setSettlementTypeDesc("下车重量");
break;
case 3:
order.setSettlementTypeDesc("按肉价结算");
break;
default:
order.setSettlementTypeDesc("未知");
break;
}
}
}
}
/**
* 填充订单关联信息(买方名称、卖方名称、创建人名称、结算方式描述)
* 用于单个订单详情查询
*/
private void fillOrderInfo(Order order) {
// 填充买方名称
@@ -358,5 +562,115 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
}
}
}
/**
* 批量导入订单
*/
@Override
@Transactional
public AjaxResult batchImportOrders(List<Map<String, Object>> orders) {
logger.info("开始批量导入订单,共{}条", orders.size());
int successCount = 0;
int failCount = 0;
List<String> failMessages = new ArrayList<>();
for (int i = 0; i < orders.size(); i++) {
Map<String, Object> orderMap = orders.get(i);
try {
// 构建Order对象
Order order = new Order();
// 设置买方ID
Object buyerIdObj = orderMap.get("buyerId");
if (buyerIdObj != null) {
order.setBuyerId(String.valueOf(buyerIdObj));
} else {
throw new RuntimeException("买方ID不能为空");
}
// 设置卖方ID
Object sellerIdObj = orderMap.get("sellerId");
if (sellerIdObj != null) {
order.setSellerId(String.valueOf(sellerIdObj));
} else {
throw new RuntimeException("卖方ID不能为空");
}
// 设置结算方式默认为1-上车重量)
Object settlementTypeObj = orderMap.get("settlementType");
if (settlementTypeObj != null) {
Integer settlementType = null;
if (settlementTypeObj instanceof Integer) {
settlementType = (Integer) settlementTypeObj;
} else if (settlementTypeObj instanceof Number) {
settlementType = ((Number) settlementTypeObj).intValue();
} else {
settlementType = Integer.parseInt(String.valueOf(settlementTypeObj));
}
if (settlementType < 1 || settlementType > 3) {
throw new RuntimeException("结算方式无效必须为1-3");
}
order.setSettlementType(settlementType);
} else {
order.setSettlementType(1); // 默认上车重量
}
// 设置约定价格
Object firmPriceObj = orderMap.get("firmPrice");
if (firmPriceObj != null) {
java.math.BigDecimal firmPrice = null;
if (firmPriceObj instanceof java.math.BigDecimal) {
firmPrice = (java.math.BigDecimal) firmPriceObj;
} else if (firmPriceObj instanceof Number) {
firmPrice = new java.math.BigDecimal(String.valueOf(firmPriceObj));
} else {
firmPrice = new java.math.BigDecimal(String.valueOf(firmPriceObj));
}
if (firmPrice.compareTo(java.math.BigDecimal.ZERO) < 0) {
throw new RuntimeException("约定价格不能小于0");
}
order.setFirmPrice(firmPrice);
} else {
throw new RuntimeException("约定价格不能为空");
}
// 设置创建人和创建时间
Integer userId = SecurityUtil.getCurrentUserId();
order.setCreatedBy(userId);
order.setCreateTime(new Date());
// 插入数据库
int result = orderMapper.insert(order);
if (result > 0) {
successCount++;
logger.info("第{}条订单导入成功订单ID{}", i + 1, order.getId());
} else {
failCount++;
failMessages.add(String.format("第%d条插入数据库失败", i + 1));
logger.error("第{}条订单导入失败:插入数据库失败", i + 1);
}
} catch (Exception e) {
failCount++;
String errorMsg = String.format("第%d条%s", i + 1, e.getMessage());
failMessages.add(errorMsg);
logger.error("第{}条订单导入失败:{}", i + 1, e.getMessage(), e);
}
}
logger.info("批量导入完成:成功{}条,失败{}条", successCount, failCount);
// 构建返回结果
Map<String, Object> result = new java.util.HashMap<>();
result.put("successCount", successCount);
result.put("failCount", failCount);
result.put("failMessages", failMessages);
if (failCount == 0) {
return AjaxResult.success("批量导入成功,共导入" + successCount + "条订单", result);
} else {
return AjaxResult.success("批量导入完成:成功" + successCount + "条,失败" + failCount + "", result);
}
}
}

View File

@@ -153,7 +153,24 @@ public class WarningLogServiceImpl extends ServiceImpl<WarningLogMapper, Warning
}
//获取当前记录的运单信息
Delivery delivery = deliveryMapper.selectById(warningLog.getDeliveryId());
Integer originalDeliveryId = warningLog.getDeliveryId();
Delivery delivery = deliveryMapper.selectById(originalDeliveryId);
// ✅ 容错处理:如果通过 delivery_id 查不到运单,尝试用预警记录的 id 作为 delivery_id 查询
// 这种情况可能是因为数据不一致,预警记录的 id 可能就是运单的 id
Integer correctDeliveryId = originalDeliveryId;
if (delivery == null && originalDeliveryId != null && !originalDeliveryId.equals(warningLog.getId())) {
log.warn("[预警详情] 通过 delivery_id={} 查询不到运单,尝试使用预警记录 id={} 作为 delivery_id 查询",
originalDeliveryId, warningLog.getId());
delivery = deliveryMapper.selectById(warningLog.getId());
if (delivery != null) {
log.info("[预警详情] ✅ 容错成功:使用预警记录 id={} 找到了运单,运单号: {}",
warningLog.getId(), delivery.getDeliveryNumber());
// 使用预警记录的 id 作为正确的 deliveryId
correctDeliveryId = warningLog.getId();
}
}
if (delivery != null) {
BeanUtils.copyProperties(delivery, warningDetailDto);
}
@@ -162,13 +179,19 @@ public class WarningLogServiceImpl extends ServiceImpl<WarningLogMapper, Warning
warningDetailDto.setInventoryJbqCount(warningLog.getInventoryJbqCount());
// ✅ 设置运单ID用于前端查询设备列表和日志
warningDetailDto.setDeliveryId(warningLog.getDeliveryId());
log.info("[预警详情] 设置运单ID: {}", warningLog.getDeliveryId());
// 使用修正后的 deliveryId如果容错成功这里会是预警记录的 id
warningDetailDto.setDeliveryId(correctDeliveryId);
log.info("[预警详情] 设置运单ID: {} (原始 delivery_id: {})", correctDeliveryId, originalDeliveryId);
//获取当前运单关联的设备信息
List<DeliveryDevice> deliveryDevices = deliveryDeviceMapper.selectList(
new LambdaQueryWrapper<DeliveryDevice>().eq(DeliveryDevice::getDeliveryId, delivery.getId())
);
List<DeliveryDevice> deliveryDevices = new ArrayList<>();
if (delivery != null) {
deliveryDevices = deliveryDeviceMapper.selectList(
new LambdaQueryWrapper<DeliveryDevice>().eq(DeliveryDevice::getDeliveryId, delivery.getId())
);
} else {
log.warn("[预警详情] 运单不存在deliveryId: {} (原始: {})", correctDeliveryId, originalDeliveryId);
}
String mainDeviceId = null;
if (CollectionUtils.isNotEmpty(deliveryDevices)) {
@@ -187,34 +210,38 @@ public class WarningLogServiceImpl extends ServiceImpl<WarningLogMapper, Warning
warningDetailDto.setJbqDeviceSn(jbqDeviceCollect);
} else {
// ✅ 如果 delivery_device 表为空,尝试从 iot_device_data 表查询(兼容旧数据)
log.info("[预警详情] delivery_device 表无数据,尝试从 iot_device_data 表查询设备");
List<IotDeviceData> iotDevices = iotDeviceDataMapper.selectList(
new LambdaQueryWrapper<IotDeviceData>().eq(IotDeviceData::getDeliveryId, delivery.getId())
);
if (CollectionUtils.isNotEmpty(iotDevices)) {
log.info("[预警详情] 从 iot_device_data 查询到 {} 个设备", iotDevices.size());
if (delivery != null) {
log.info("[预警详情] delivery_device 表无数据,尝试从 iot_device_data 表查询设备");
List<IotDeviceData> iotDevices = iotDeviceDataMapper.selectList(
new LambdaQueryWrapper<IotDeviceData>().eq(IotDeviceData::getDeliveryId, delivery.getId())
);
// 查找主机设备类型1或4
List<String> serverCollect = iotDevices.stream()
.filter(device -> device.getDeviceType() != null &&
(device.getDeviceType() == 1 || device.getDeviceType() == 4))
.map(IotDeviceData::getDeviceId)
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(serverCollect)) {
mainDeviceId = serverCollect.get(0);
warningDetailDto.setServerDeviceSn(mainDeviceId);
log.info("[预警详情] 找到主机设备: {}", mainDeviceId);
if (CollectionUtils.isNotEmpty(iotDevices)) {
log.info("[预警详情] 从 iot_device_data 查询到 {} 个设备", iotDevices.size());
// 查找主机设备类型1或4
List<String> serverCollect = iotDevices.stream()
.filter(device -> device.getDeviceType() != null &&
(device.getDeviceType() == 1 || device.getDeviceType() == 4))
.map(IotDeviceData::getDeviceId)
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(serverCollect)) {
mainDeviceId = serverCollect.get(0);
warningDetailDto.setServerDeviceSn(mainDeviceId);
log.info("[预警详情] 找到主机设备: {}", mainDeviceId);
}
// 查找耳标设备类型2
List<String> jbqDeviceCollect = iotDevices.stream()
.filter(device -> device.getDeviceType() != null && device.getDeviceType() == 2)
.map(IotDeviceData::getDeviceId)
.collect(Collectors.toList());
warningDetailDto.setJbqDeviceSn(jbqDeviceCollect);
} else {
log.warn("[预警详情] iot_device_data 表也无设备数据");
}
// 查找耳标设备类型2
List<String> jbqDeviceCollect = iotDevices.stream()
.filter(device -> device.getDeviceType() != null && device.getDeviceType() == 2)
.map(IotDeviceData::getDeviceId)
.collect(Collectors.toList());
warningDetailDto.setJbqDeviceSn(jbqDeviceCollect);
} else {
log.warn("[预警详情] iot_device_data 表也无设备数据");
log.warn("[预警详情] 运单不存在,无法查询设备信息");
}
}

View File

@@ -82,5 +82,37 @@ public class DeliveryLogVo {
*/
private String createByDesc;
/**
* 起始地
*/
private String startLocation;
/**
* 送达目的地
*/
private String endLocation;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
/**
* 预计送达时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date estimatedDeliveryTime;
/**
* 司机姓名
*/
private String driverName;
/**
* 创建人(从 sys_user 表关联查询)
*/
private String createByName;
}

View File

@@ -40,10 +40,25 @@ public class XxlJobConfig {
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Value("${xxl.job.executor.connect-timeout:30000}")
private int connectTimeout;
@Value("${xxl.job.executor.read-timeout:30000}")
private int readTimeout;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
// 设置 HTTP 连接超时和读取超时时间(通过系统属性)
// XXL-Job 内部使用 HttpsURLConnection通过系统属性可以设置默认超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", String.valueOf(connectTimeout));
System.setProperty("sun.net.client.defaultReadTimeout", String.valueOf(readTimeout));
logger.info(">>>>>>>>>>> xxl-job timeout config: connectTimeout={}ms, readTimeout={}ms",
connectTimeout, readTimeout);
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);

View File

@@ -0,0 +1,34 @@
package com.aiotagro.cattletrade.job;
import com.aiotagro.cattletrade.business.service.DeliveryYingyanSyncService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 百度鹰眼轨迹同步任务
*/
@Component
public class BaiduYingyanSyncJob {
private static final Logger logger = LoggerFactory.getLogger(BaiduYingyanSyncJob.class);
@Autowired
private DeliveryYingyanSyncService deliveryYingyanSyncService;
/**
* 每两分钟同步一次轨迹
*/
@Scheduled(initialDelay = 30_000, fixedDelay = 120_000)
public void syncDeliveryTrack() {
try {
logger.debug("开始执行运单鹰眼轨迹同步任务");
deliveryYingyanSyncService.syncActiveDeliveries();
} catch (Exception e) {
logger.error("运单鹰眼轨迹同步任务执行失败", e);
}
}
}

View File

@@ -0,0 +1,28 @@
package com.aiotagro.common.core.constant;
/**
* 百度鹰眼常量配置
*
* <p>注意AK 与 ServiceId 根据业务要求写死在后端,禁止透出给前端。</p>
*/
public final class BaiduYingyanConstants {
private BaiduYingyanConstants() {
}
/**
* 百度鹰眼控制台申请的 AK
*/
public static final String AK = "xITbC7jegaAAuu4m9jC2Zx6eFbQJ29Rj";
/**
* 百度鹰眼服务 ID
*/
public static final long SERVICE_ID = 242517L;
/**
* 百度鹰眼 API 基础路径
*/
public static final String BASE_URL = "https://yingyan.baidu.com/api/v3";
}

View File

@@ -99,6 +99,10 @@ xxl:
logpath: /data/applogs/xxl-job/jobhandler
# 日志保存时间
logretentiondays: 30
# 连接超时时间毫秒默认30秒
connect-timeout: 30000
# 读取超时时间毫秒默认30秒
read-timeout: 30000
address:
ip:
# 日志配置

View File

@@ -108,6 +108,10 @@ xxl:
logpath: /data/applogs/xxl-job/jobhandler
# 日志保存时间
logretentiondays: 30
# 连接超时时间毫秒默认30秒
connect-timeout: 30000
# 读取超时时间毫秒默认30秒
read-timeout: 30000
address:
ip:

View File

@@ -101,6 +101,10 @@ xxl:
logpath: /data/applogs/xxl-job/jobhandler
# 日志保存时间
logretentiondays: 30
# 连接超时时间毫秒默认30秒
connect-timeout: 30000
# 读取超时时间毫秒默认30秒
read-timeout: 30000
address:
ip:
# 日志配置 - 生产环境应使用更高的日志级别以提升性能

View File

@@ -0,0 +1,12 @@
USE cattletrade;
-- 为运送清单表新增百度鹰眼同步字段
ALTER TABLE `delivery`
ADD COLUMN `yingyan_entity_name` VARCHAR(64) NULL DEFAULT NULL COMMENT '百度鹰眼终端名称';
ALTER TABLE `delivery`
ADD COLUMN `yingyan_last_sync_time` DATETIME NULL DEFAULT NULL COMMENT '百度鹰眼最后同步时间';
ALTER TABLE `delivery`
ADD COLUMN `arrival_time` DATETIME NULL DEFAULT NULL COMMENT '自动判定的到达时间';

View File

@@ -64,31 +64,38 @@
wl.warning_time,
wl.inventory_jbq_count,
su.name as createByName
FROM delivery d inner join warning_log wl on d.id = wl.delivery_id
left join sys_user su on d.created_by = su.id
FROM delivery d
INNER JOIN warning_log wl ON d.id = wl.delivery_id
LEFT JOIN sys_user su ON d.created_by = su.id
<where>
wl.warning_type in (2,3,4,5,6,7,8,9)
and wl.id in (select max(id) from warning_log where delivery_id = d.id group by warning_type)
wl.warning_type IN (2,3,4,5,6,7,8,9)
AND wl.id IN (
SELECT MAX(id)
FROM warning_log
WHERE delivery_id = d.id
AND warning_type IN (2,3,4,5,6,7,8,9)
GROUP BY warning_type
)
<if test="dto.deliveryNumber != null and '' != dto.deliveryNumber">
and d.delivery_number like concat('%', #{dto.deliveryNumber}, '%')
AND d.delivery_number LIKE CONCAT('%', #{dto.deliveryNumber}, '%')
</if>
<if test="dto.licensePlate != null and '' != dto.licensePlate">
and d.license_plate like concat('%', #{dto.licensePlate}, '%')
AND d.license_plate LIKE CONCAT('%', #{dto.licensePlate}, '%')
</if>
<if test="dto.startTime != null and '' != dto.startTime">
and d.create_time <![CDATA[ >= ]]> #{dto.startTime}
AND d.create_time <![CDATA[ >= ]]> #{dto.startTime}
</if>
<if test="dto.endTime != null and '' != dto.endTime">
and d.create_time <![CDATA[ <= ]]> #{dto.endTime}
AND d.create_time <![CDATA[ <= ]]> #{dto.endTime}
</if>
<if test="dto.warningType != null">
and wl.warning_type = #{dto.warningType}
AND wl.warning_type = #{dto.warningType}
</if>
<if test="dto.status != null">
and d.status = #{dto.status}
AND d.status = #{dto.status}
</if>
</where>
order by wl.warning_time desc
ORDER BY wl.warning_time DESC
</select>
</mapper>

View File

@@ -55,4 +55,33 @@
</foreach>
</insert>
<select id="listLogsForYingyan" resultType="com.aiotagro.cattletrade.business.entity.JbqClientLog">
SELECT
<include refid="Base_Column_List"/>
FROM
jbq_client_log
<where>
<if test="deviceIds != null and deviceIds.size > 0">
device_id IN
<foreach collection="deviceIds" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="startTime != null">
AND update_time <![CDATA[>=]]> #{startTime}
</if>
<if test="endTime != null">
AND update_time <![CDATA[<=]]> #{endTime}
</if>
AND latitude IS NOT NULL
AND latitude != ''
AND longitude IS NOT NULL
AND longitude != ''
</where>
ORDER BY update_time ASC
<if test="limit != null">
LIMIT #{limit}
</if>
</select>
</mapper>

View File

@@ -46,4 +46,33 @@
</foreach>
</insert>
<select id="listLogsForYingyan" resultType="com.aiotagro.cattletrade.business.entity.JbqServerLog">
SELECT
<include refid="Base_Column_List"/>
FROM
jbq_server_log
<where>
<if test="deviceIds != null and deviceIds.size > 0">
device_id IN
<foreach collection="deviceIds" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="startTime != null">
AND update_time <![CDATA[>=]]> #{startTime}
</if>
<if test="endTime != null">
AND update_time <![CDATA[<=]]> #{endTime}
</if>
AND latitude IS NOT NULL
AND latitude != ''
AND longitude IS NOT NULL
AND longitude != ''
</where>
ORDER BY update_time ASC
<if test="limit != null">
LIMIT #{limit}
</if>
</select>
</mapper>

View File

@@ -57,4 +57,33 @@
</foreach>
</insert>
<select id="listLogsForYingyan" resultType="com.aiotagro.cattletrade.business.entity.XqClientLog">
SELECT
<include refid="Base_Column_List"/>
FROM
xq_client_log
<where>
<if test="deviceIds != null and deviceIds.size > 0">
device_id IN
<foreach collection="deviceIds" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="startTime != null">
AND update_time <![CDATA[>=]]> #{startTime}
</if>
<if test="endTime != null">
AND update_time <![CDATA[<=]]> #{endTime}
</if>
AND latitude IS NOT NULL
AND latitude != ''
AND longitude IS NOT NULL
AND longitude != ''
</where>
ORDER BY update_time ASC
<if test="limit != null">
LIMIT #{limit}
</if>
</select>
</mapper>