Files
park/application/api/controller/YqDataHandle.php

1155 lines
53 KiB
PHP
Raw Permalink Normal View History

2025-12-01 11:19:23 +08:00
<?php
namespace app\api\controller;
use app\common\controller\Api;
use think\Config;
use think\Db;
use think\Request;
use think\Session;
class YqDataHandle extends Api {
protected $noNeedLogin = ['*'];
protected $noNeedRight = ['*'];
//车辆信息处理
public function analog_trigger(){
// $ipt = input();
// $body = $ipt['body'];
$stttt = '{"dangerousGoodsType":"易燃液体,腐蚀性物质","certificateEffdate":"20191216","transCertificateCode":"500115068184","ownerName":"重庆戈林物流有限责任公司","licenseIssueOrganCode":"长寿区运管所","dataType":"position","certificateExpdate":"20231216","transCertificateWord":"渝","position":[{"vehicleNo":"渝A654321","positionTime":1609317985,"latitudeWgs84":29.8176429,"latitude":"29.8050100","longitudeWgs84":107.0190752,"vec1":0.0,"alititude":247.0,"plateColor":2,"longitude":"107.0072050","direction":341}],"vinNo":"LGAG4DY34B8005552","vehicleType":"Q11","businessScopeName":"危险品货物运输(第3类),危险品货物运输(第8类)"}';
$body = json_decode($stttt,true);
if ($body){
$str = $this->getWgs84(number_format($body['position'][0]['longitude'],7).','.number_format($body['position'][0]['latitude'],7));
$str['lon'] = '106.9822520';
$str['lat'] = '29.8299610';
$point=[
'lng'=>$str['lon'],
'lat'=>$str['lat'],
];
}else{
$this->alertMsg(300,'未接收到推送数据!');
}
//TODO 外层(大)围栏坐标集-只用判断,暂废弃20210323
//$big_path = "107.062736,29.891181;107.061300,29.897795;107.060726,29.901168;107.056430,29.902360;107.048547,29.902280;107.040363,29.903125;107.034322,29.903390;107.025101,29.904721;107.016010,29.905977;107.007779,29.906362;106.999695,29.905864;106.992628,29.904575;106.985715,29.901749;106.980395,29.898133;106.975513,29.893491;106.968060,29.886804;106.961906,29.880971;106.956901,29.875763;106.933588,29.853736;106.925125,29.841266;106.915339,29.824596;106.903938,29.804884;106.899749,29.793657;106.896866,29.777028;106.902065,29.771937;106.910574,29.767958;106.921226,29.763030;106.929984,29.760690;106.942019,29.758331;106.957773,29.738776;106.966653,29.733627;106.992518,29.718204;107.007523,29.715230;107.043405,29.707971;107.056169,29.709077;107.070341,29.713438;107.084227,29.718169;107.076353,29.742014;107.080633,29.759133;107.096674,29.776956;107.087634,29.780280;107.086198,29.792803;107.085341,29.801318;107.081194,29.809038;107.068617,29.824131;107.060616,29.839669;107.067773,29.852063;107.067761,29.872731;107.065890,29.883719;107.062879,29.890933";
//TODO 内层围栏坐标集20210323
// $max_path = "107.01527,29.895601;106.997876,29.89281;106.991391,29.887939;106.985853,29.880405;106.981835,29.871026;106.960543,29.849142;106.950737,29.83564;106.945862,29.824089;106.943563,29.814148;106.945054,29.799754;106.942764,29.783607;106.970332,29.766519;106.983273,29.751322;107.017612,29.737776;107.050425,29.734115;107.064458,29.737683;107.064744,29.739318;107.058942,29.748447;107.055205,29.762378;107.071581,29.77504;107.086248,29.789923;107.088111,29.796775;107.084106,29.807387;107.080959,29.81142;107.068298,29.813474;107.06422,29.813406;107.060435,29.82112;107.050628,29.827207;107.04726,29.828623;107.046543,29.829966;107.04701,29.830878;107.04712,29.834263;107.044827,29.838826;107.053757,29.842875;107.056479,29.849047;107.057124,29.85153;107.056802,29.853468;107.060415,29.86047;107.061665,29.864841;107.054919,29.881592;107.052508,29.894119;107.040451,29.898403;107.028579,29.89536;107.014946,29.895825;106.997804,29.89278";
$max_path_json = '[{"lng":"107.01527","lat":"29.895601"},{"lng":"106.997876","lat":"29.89281"},{"lng":"106.991391","lat":"29.887939"},{"lng":"106.985853","lat":"29.880405"},{"lng":"106.981835","lat":"29.871026"},{"lng":"106.960543","lat":"29.849142"},{"lng":"106.950737","lat":"29.83564"},{"lng":"106.945862","lat":"29.824089"},{"lng":"106.943563","lat":"29.814148"},{"lng":"106.945054","lat":"29.799754"},{"lng":"106.942764","lat":"29.783607"},{"lng":"106.970332","lat":"29.766519"},{"lng":"106.983273","lat":"29.751322"},{"lng":"107.017612","lat":"29.737776"},{"lng":"107.050425","lat":"29.734115"},{"lng":"107.064458","lat":"29.737683"},{"lng":"107.064744","lat":"29.739318"},{"lng":"107.058942","lat":"29.748447"},{"lng":"107.055205","lat":"29.762378"},{"lng":"107.071581","lat":"29.77504"},{"lng":"107.086248","lat":"29.789923"},{"lng":"107.088111","lat":"29.796775"},{"lng":"107.084106","lat":"29.807387"},{"lng":"107.080959","lat":"29.81142"},{"lng":"107.068298","lat":"29.813474"},{"lng":"107.06422","lat":"29.813406"},{"lng":"107.060435","lat":"29.82112"},{"lng":"107.050628","lat":"29.827207"},{"lng":"107.04726","lat":"29.828623"},{"lng":"107.046543","lat":"29.829966"},{"lng":"107.04701","lat":"29.830878"},{"lng":"107.04712","lat":"29.834263"},{"lng":"107.044827","lat":"29.838826"},{"lng":"107.053757","lat":"29.842875"},{"lng":"107.056479","lat":"29.849047"},{"lng":"107.057124","lat":"29.85153"},{"lng":"107.056802","lat":"29.853468"},{"lng":"107.060415","lat":"29.86047"},{"lng":"107.061665","lat":"29.864841"},{"lng":"107.054919","lat":"29.881592"},{"lng":"107.052508","lat":"29.894119"},{"lng":"107.040451","lat":"29.898403"},{"lng":"107.028579","lat":"29.89536"},{"lng":"107.014946","lat":"29.895825"},{"lng":"106.997804","lat":"29.89278"}]';
$max_path_arr = json_decode($max_path_json,true);
//周界二级区域
$one_region = Db::name('perimeter')->where(['region_lv'=>2,'is_del'=>1])->select();
foreach ($one_region as $onek => $onev){
$lv1 = explode(';',$onev['info']);
$lv1_arr = [];
foreach ($lv1 as $lv1K => $lv1V){
$ar = explode(',',$lv1V);
$lv1_arr[] = [
'lng' => $ar[0],
'lat' => $ar[1],
];
}
$lv1s = $this->is_point_in_polygon($point,$lv1_arr);
if ($lv1s){
//三级周界判断
$two_region = Db::name('perimeter')->where(['pid'=>$onev['id'],'is_del'=>1])->select();
if (!empty($two_region)){
foreach ($two_region as $twok => $twov){
if($twov['info']){
$lv2 = explode(';',$twov['info']);
$lv2_arr = [];
foreach ($lv2 as $lv2K => $lv2V){
$ar = explode(',',$lv2V);
$lv2_arr[] = [
'lng' => $ar[0],
'lat' => $ar[1],
];
}
$lv2s = $this->is_point_in_polygon($point,$lv2_arr);
if ($lv2s){
$msgs = $this->ruleJudge($twov,$body,$str);
$data['msgs'] = $msgs;
$this->alertMsg(200,'处理完成!',$data);
}
}
}
}else{
$this->ruleJudge($onev,$body,$str);
$this->alertMsg(200,'车辆在二级区域,车辆、报警数据更新!');
}
}
}
$vehicleInfo = $body['position'][0];
$data = [
'longitude' => $str['lon'],
'latitude' => $str['lat'],
'longitude_84' => $vehicleInfo['longitudeWgs84'],
'latitude_84' => $vehicleInfo['latitudeWgs84'],
'alititude' => $vehicleInfo['alititude'],
'vec1' => $vehicleInfo['vec1'],
'direction' => $vehicleInfo['direction'],
'positionTime' => date('Y-m-d H:i:s',$vehicleInfo['positionTime']),
// 'certificateEffdate' => $dataInfo['certificateEffdate'],
// 'certificateExpdate' => $dataInfo['certificateExpdate'],
// 'transCertificateCode' => $dataInfo['transCertificateCode'],
// 'ownerName' => $dataInfo['ownerName'],
// 'licenseIssueOrganCode' => $dataInfo['licenseIssueOrganCode'],
// 'transCertificateWord' => $dataInfo['transCertificateWord'],
// 'vinNo' => $dataInfo['vinNo'],
// 'vehicleType' => $dataInfo['vehicleType'],
// 'businessScopeName' => $dataInfo['businessScopeName'],
'is_del' => 1,
'perimeter_id' => '',//车辆当前定位
'vehicle_type' => 0,//车辆类型展示状态(-1区域外行驶0正常1超速2禁行3禁停4未上报5超最大数量
'create_time' => date('Y-m-d H:i:s'),
];
$vehicle = Db::name($this->api_vehicle)->where('vehicleNo',$vehicleInfo['vehicleNo'])->field('id,longitude,latitude')->find();
if ($vehicle['id']){
$new_distance = $this->getDistance($str['lon'],$str['lat'],$vehicle['latitude'],$vehicle['longitude']);
$data['old_longitude'] = $vehicle['longitude'];
$data['old_latitude'] = $vehicle['latitude'];
$data['distance'] = 0;
Db::name($this->api_vehicle)->where('vehicleNo',$vehicleInfo['vehicleNo'])->update($data);
}else{
$data['vehicleNo'] = $vehicleInfo['vehicleNo'];
$data['plateColor'] = $vehicleInfo['plateColor'];
Db::name($this->api_vehicle)->insert($data);
}
$this->alertMsg(200,'车辆在一级区域,车辆数据更新!');
die;
//保存当前坐标点
// $line = [
// 'vehicleNo' => $v['vehicleNo'],
// 'coordinate' => number_format($longitude,7).','.number_format($latitude,7),
// 'create_time' => date('Y-m-d H:i:s'),
// ];
// Db::name('vehicle_line')->insert($line);
//最大区域判断 new一级区域判断
foreach ($one_region as $onek => $onev){
$lv1 = explode(';',$onev['info']);
$lv1_arr = [];
foreach ($lv1 as $lv1K => $lv1V){
$ar = explode(',',$lv1V);
$lv1_arr[] = [
'lng' => $ar[0],
'lat' => $ar[1],
];
}
$lv1s = $this->is_point_in_polygon($point,$lv1_arr);
if ($lv1s){
//二级区域判断
$two_region = Db::name('perimeter')->where(['pid'=>$onev['id'],'is_del'=>1])->select();
if (!empty($two_region)){
foreach ($two_region as $twok => $twov){
if($twov['info']){
$lv2 = explode(';',$twov['info']);
$lv2_arr = [];
foreach ($lv2 as $lv2K => $lv2V){
$ar = explode(',',$lv2V);
$lv2_arr[] = [
'lng' => $ar[0],
'lat' => $ar[1],
];
}
$lv2s = $this->is_point_in_polygon($point,$lv2_arr);
if ($lv2s){
//三级级区域判断
$three_region = Db::name('perimeter')->where(['pid'=>$twov['id'],'is_del'=>1])->select();
if (!empty($three_region)){
foreach ($three_region as $threek => $threev){
if($threev['info']){
$lv3 = explode(';',$threev['info']);
$lv3_arr = [];
foreach ($lv3 as $lv3K => $lv3V){
$ar = explode(',',$lv3V);
$lv3_arr[] = [
'lng' => $ar[0],
'lat' => $ar[1],
];
}
$lv3s = $this->is_point_in_polygon($point,$lv3_arr);
if ($lv3s){
$this->ruleJudge($threev,$body,$str);
$this->alertMsg(200,'车辆在三级区域,车辆、报警数据更新!');
}
}
}
}else{
$this->ruleJudge($twov,$body,$str);
$this->alertMsg(200,'车辆在二级区域,车辆、报警数据更新!');
}
}
}
}
}else{
$this->ruleJudge($onev,$body,$str);
$this->alertMsg(200,'车辆在一级区域,车辆、报警数据更新!');
}
}else{
}
}
$vehicle = Db::name($this->api_vehicle)->where('vehicleNo',$v['vehicleNo'])->field('id,longitude,latitude')->find();
$data = [
'longitude' => $longitude,
'latitude' => $latitude,
'longitude_84' => $v['longitudeWgs84'],
'latitude_84' => $v['latitudeWgs84'],
'alititude' => $v['alititude'],
'vec1' => $v['vec1'],
'direction' => $v['direction'],
'positionTime' => date('Y-m-d H:i:s',$v['positionTime']),
'vehicle_type' => 0,
// 'certificateEffdate' => $body['certificateEffdate'],
// 'certificateExpdate' => $body['certificateExpdate'],
// 'transCertificateCode' => $body['transCertificateCode'],
// 'ownerName' => $body['ownerName'],
// 'licenseIssueOrganCode' => $body['licenseIssueOrganCode'],
// 'transCertificateWord' => $body['transCertificateWord'],
// 'vinNo' => $body['vinNo'],
// 'vehicleType' => $body['vehicleType'],
// 'businessScopeName' => $body['businessScopeName'],
'perimeter_id' => 0,
'is_del' => 1,
'create_time' => date('Y-m-d H:i:s'),
];
if ($vehicle['id']){
$new_distance = $this->getDistance($latitude,$longitude,$vehicle['latitude'],$vehicle['longitude']);
if ($new_distance >= 15000){
$event_data = [
'name' => 'GPS瞬移【间距'.$new_distance.'】',
'type' => '7',//7为 GPS瞬移
'perimeter_point' => $latitude.','.$longitude,
'license' => $v['vehicleNo'],
'trigger_type' => 1,
'create_time' => date('Y-m-d H:i:s'),
'res_department' => 1,
];
Db::name($this->api_call_the_police)->insertGetId($event_data);
}
$data['old_longitude'] = $vehicle['longitude'];
$data['old_latitude'] = $vehicle['latitude'];
$data['distance'] = $new_distance;
Db::name($this->api_vehicle)->where('vehicleNo',$v['vehicleNo'])->update($data);
}else{
$data['vehicleNo'] = $v['vehicleNo'];
$data['plateColor'] = $v['plateColor'];
Db::name($this->api_vehicle)->insert($data);
}
$this->alertMsg(200,'车辆在一级区域外!');
}
/**
* 同步长寿接口车辆数据到本地数据库
* 接收长寿接口格式的车辆数据并写入yq_vehicle表
* 同时根据企业周界规则进行校验并生成报警
*/
public function sync_vehicle()
{
$input = file_get_contents('php://input');
$body = json_decode($input, true);
// 调试日志
if (empty($input)) {
$this->error('请求体为空!');
}
if ($body === null) {
$this->error('JSON解析失败请求内容' . substr($input, 0, 500));
}
if (empty($body)) {
$this->error('请求体为空或JSON解析失败');
}
if (!isset($body['data'])) {
$this->error('数据格式错误缺少data字段接收到的数据' . json_encode($body, JSON_UNESCAPED_UNICODE));
}
// 如果data为空数组直接返回成功没有车辆数据需要同步
if (empty($body['data']) || !is_array($body['data'])) {
$this->success('同步完成(无车辆数据)', ['success' => 0, 'fail' => 0, 'alarm_count' => 0]);
}
$insertData = [];
$updateData = [];
$vehicleList = []; // 用于规则校验的车辆列表
$successCount = 0;
$failCount = 0;
$alarmCount = 0;
$now = date('Y-m-d H:i:s');
// 收集所有车牌号
$allVehNos = [];
foreach ($body['data'] as $item) {
if (!empty($item['posList'])) {
foreach ($item['posList'] as $vehicle) {
if (!empty($vehicle['vehNo'])) {
$allVehNos[] = $vehicle['vehNo'];
}
}
}
}
// 批量查询已存在的车辆
$existVehicles = [];
if (!empty($allVehNos)) {
$existList = Db::name('vehicle')->where('vehicleNo', 'in', $allVehNos)->column('id,longitude,latitude', 'vehicleNo');
$existVehicles = $existList ?: [];
}
foreach ($body['data'] as $item) {
if (empty($item['posList'])) {
continue;
}
foreach ($item['posList'] as $vehicle) {
// 验证必要字段
if (empty($vehicle['vehNo']) || empty($vehicle['lon']) || empty($vehicle['lat'])) {
$failCount++;
continue;
}
// GCJ02坐标
$longitude = number_format($vehicle['lon'], 7);
$latitude = number_format($vehicle['lat'], 7);
// 处理定位时间
$positionTime = $now;
if (isset($vehicle['posTime'])) {
if (is_numeric($vehicle['posTime'])) {
$positionTime = date('Y-m-d H:i:s', $vehicle['posTime']);
} else {
$positionTime = $vehicle['posTime'];
}
}
$vehNo = $vehicle['vehNo'];
$speed = isset($vehicle['v1']) ? floatval($vehicle['v1']) : 0;
// 收集用于规则校验的车辆数据
$vehicleList[] = [
'vehNo' => $vehNo,
'longitude' => $longitude,
'latitude' => $latitude,
'speed' => $speed,
'color' => isset($vehicle['color']) ? $vehicle['color'] : 2,
];
if (isset($existVehicles[$vehNo])) {
// 更新数据
$updateData[] = [
'vehicleNo' => $vehNo,
'longitude' => $longitude,
'latitude' => $latitude,
'longitude_84' => isset($vehicle['originalLon']) ? $vehicle['originalLon'] : $vehicle['lon'],
'latitude_84' => isset($vehicle['originalLat']) ? $vehicle['originalLat'] : $vehicle['lat'],
'vec1' => $speed,
'direction' => isset($vehicle['dir']) ? $vehicle['dir'] : 0,
'positionTime' => $positionTime,
'is_del' => 1,
'vehicle_type' => 0,
'qr_color' => isset($vehicle['color']) ? $vehicle['color'] : 2,
'perimeter_id' => 0,
'old_longitude' => $existVehicles[$vehNo]['longitude'],
'old_latitude' => $existVehicles[$vehNo]['latitude'],
];
} else {
// 新增数据
$insertData[] = [
'vehicleNo' => $vehNo,
'plateColor' => isset($vehicle['color']) ? $vehicle['color'] : 2,
'longitude' => $longitude,
'latitude' => $latitude,
'longitude_84' => isset($vehicle['originalLon']) ? $vehicle['originalLon'] : $vehicle['lon'],
'latitude_84' => isset($vehicle['originalLat']) ? $vehicle['originalLat'] : $vehicle['lat'],
'vec1' => $speed,
'direction' => isset($vehicle['dir']) ? $vehicle['dir'] : 0,
'positionTime' => $positionTime,
'is_del' => 1,
'vehicle_type' => 0,
'qr_color' => isset($vehicle['color']) ? $vehicle['color'] : 2,
'perimeter_id' => 0,
'create_time' => $now,
];
}
}
}
try {
// 批量插入新车辆
if (!empty($insertData)) {
Db::name('vehicle')->insertAll($insertData);
$successCount += count($insertData);
}
// 批量更新已有车辆
foreach ($updateData as $data) {
$vehNo = $data['vehicleNo'];
unset($data['vehicleNo']);
Db::name('vehicle')->where('vehicleNo', $vehNo)->update($data);
$successCount++;
}
// 保存车辆轨迹数据到 vehicle_line 表
$trajectoryResult = $this->saveVehicleTrajectory($body['data'], $now);
if ($trajectoryResult['fail'] > 0) {
\think\Log::warning('车辆轨迹保存有失败: ' . json_encode($trajectoryResult, JSON_UNESCAPED_UNICODE));
}
// 执行规则校验并生成报警
$alarmCount = $this->checkVehicleRulesAndGenerateAlarms($vehicleList, $now);
} catch (\Exception $e) {
$failCount = count($insertData) + count($updateData);
$successCount = 0;
$lastError = $e->getMessage();
}
$result = [
'success' => $successCount,
'fail' => $failCount,
'alarm_count' => $alarmCount,
'trajectory' => isset($trajectoryResult) ? [
'success' => $trajectoryResult['success'],
'fail' => $trajectoryResult['fail']
] : null
];
if (isset($lastError)) {
$result['lastError'] = $lastError;
}
// 调试日志:记录数据格式(仅在前几次调用时记录)
static $debugCount = 0;
if ($debugCount < 3) {
$debugData = [
'data_structure' => [
'has_data' => isset($body['data']),
'data_type' => isset($body['data']) ? gettype($body['data']) : 'null',
'data_count' => isset($body['data']) && is_array($body['data']) ? count($body['data']) : 0,
'first_item_keys' => isset($body['data'][0]) && is_array($body['data'][0]) ? array_keys($body['data'][0]) : null,
'has_posList' => isset($body['data'][0]['posList']),
'posList_count' => isset($body['data'][0]['posList']) && is_array($body['data'][0]['posList']) ? count($body['data'][0]['posList']) : 0,
],
'trajectory_result' => isset($trajectoryResult) ? $trajectoryResult : null
];
\think\Log::info('sync_vehicle 接收数据格式: ' . json_encode($debugData, JSON_UNESCAPED_UNICODE));
$debugCount++;
}
$this->success('同步完成', $result);
}
/**
* 根据企业周界规则校验车辆并生成报警
* @param array $vehicleList 车辆列表
* @param string $now 当前时间
* @return int 报警数量
*/
private function checkVehicleRulesAndGenerateAlarms($vehicleList, $now)
{
if (empty($vehicleList)) {
return 0;
}
// 获取所有有坐标的企业周界配置
$perimeters = Db::name('perimeter')
->where('is_del', 1)
->where('info', '<>', '')
->field('id,name,info,is_stop,is_ban,is_hazard,stop_vehicle,feasible_vehicle,max_speed')
->select();
if (empty($perimeters)) {
return 0;
}
// 预处理周界坐标
$perimeterPolygons = [];
foreach ($perimeters as $perimeter) {
$polygon = $this->parsePerimeterCoords($perimeter['info']);
if (!empty($polygon)) {
$perimeterPolygons[] = [
'perimeter' => $perimeter,
'polygon' => $polygon
];
}
}
$alarms = [];
// 统计每个周界内的停止车辆数和行驶车辆数
$perimeterVehicleStats = [];
// 第一次遍历:统计每个周界内的车辆数
foreach ($vehicleList as $vehicle) {
$point = [
'lng' => $vehicle['longitude'],
'lat' => $vehicle['latitude']
];
foreach ($perimeterPolygons as $pp) {
$perimeterId = $pp['perimeter']['id'];
if ($this->is_point_in_polygon($point, $pp['polygon'])) {
if (!isset($perimeterVehicleStats[$perimeterId])) {
$perimeterVehicleStats[$perimeterId] = [
'stop_count' => 0,
'drive_count' => 0,
'vehicles' => []
];
}
if ($vehicle['speed'] == 0) {
$perimeterVehicleStats[$perimeterId]['stop_count']++;
} else {
$perimeterVehicleStats[$perimeterId]['drive_count']++;
}
$perimeterVehicleStats[$perimeterId]['vehicles'][] = $vehicle;
}
}
}
// 第二次遍历:根据规则生成报警
foreach ($perimeterPolygons as $pp) {
$perimeter = $pp['perimeter'];
$perimeterId = $perimeter['id'];
if (!isset($perimeterVehicleStats[$perimeterId])) {
continue;
}
$stats = $perimeterVehicleStats[$perimeterId];
foreach ($stats['vehicles'] as $vehicle) {
$vehicleAlarms = [];
// 1. 超速校验
if ($perimeter['max_speed'] > 0 && $vehicle['speed'] > $perimeter['max_speed']) {
$vehicleAlarms[] = [
'name' => '超速行驶(限速' . $perimeter['max_speed'] . 'km/h,实际' . $vehicle['speed'] . 'km/h)',
'type' => 1, // 超速
];
}
// 2. 禁行区域校验
if ($perimeter['is_ban'] == 1 && $vehicle['speed'] > 0) {
$vehicleAlarms[] = [
'name' => '进入禁行区域(' . $perimeter['name'] . ')',
'type' => 2, // 禁行
];
}
// 3. 禁停区域校验车辆速度为0表示停止
if ($perimeter['is_stop'] == 1 && $vehicle['speed'] == 0) {
$vehicleAlarms[] = [
'name' => '在禁停区域停车(' . $perimeter['name'] . ')',
'type' => 3, // 禁停
];
}
// 4. 危险源区域预警
if ($perimeter['is_hazard'] == 1) {
$vehicleAlarms[] = [
'name' => '进入危险源区域(' . $perimeter['name'] . ')',
'type' => 6, // 危险源预警
];
}
// 5. 停车数量超限校验
if ($perimeter['stop_vehicle'] > 0 &&
$stats['stop_count'] > $perimeter['stop_vehicle'] &&
$vehicle['speed'] == 0) {
$vehicleAlarms[] = [
'name' => '停车数量超限(限' . $perimeter['stop_vehicle'] . '辆,当前' . $stats['stop_count'] . '辆)',
'type' => 5, // 超最大数量
];
}
// 6. 行驶车辆超限校验
if ($perimeter['feasible_vehicle'] > 0 &&
$stats['drive_count'] > $perimeter['feasible_vehicle'] &&
$vehicle['speed'] > 0) {
$vehicleAlarms[] = [
'name' => '行驶车辆超限(限' . $perimeter['feasible_vehicle'] . '辆,当前' . $stats['drive_count'] . '辆)',
'type' => 5, // 超最大数量
];
}
// 生成报警记录
foreach ($vehicleAlarms as $alarm) {
// 检查是否已存在相同的未处理报警(防止重复报警)
// 同一车辆、同一类型、同一天只报警一次
$existAlarm = Db::name('call_the_police')
->where('license', $vehicle['vehNo'])
->where('type', $alarm['type'])
->where('is_del', 1)
->whereTime('create_time', 'd')
->find();
if (!$existAlarm) {
$alarms[] = [
'name' => $alarm['name'],
'type' => $alarm['type'],
'license' => $vehicle['vehNo'],
'perimeter_point' => $vehicle['longitude'] . ',' . $vehicle['latitude'],
'trigger_type' => 1, // 自动触发
'is_del' => 1,
'create_time' => $now,
'res_department' => 1,
];
}
}
}
}
// 批量插入报警记录
if (!empty($alarms)) {
Db::name('call_the_police')->insertAll($alarms);
}
return count($alarms);
}
/**
* 解析周界坐标字符串为多边形数组
* @param string $info 坐标字符串,格式: "lng1,lat1;lng2,lat2;..."
* @return array 多边形坐标数组
*/
private function parsePerimeterCoords($info)
{
if (empty($info)) {
return [];
}
$polygon = [];
$points = explode(';', $info);
foreach ($points as $point) {
$coords = explode(',', $point);
if (count($coords) >= 2) {
$polygon[] = [
'lng' => trim($coords[0]),
'lat' => trim($coords[1])
];
}
}
return $polygon;
}
/**
* 测试接口:检查周界配置和车辆位置
* 用于调试报警生成功能
*/
public function test_alarm_check()
{
$result = [];
// 1. 查询所有有坐标的周界
$perimeters = Db::name('perimeter')
->where('is_del', 1)
->where('info', '<>', '')
->field('id,name,info,is_stop,is_ban,is_hazard,stop_vehicle,feasible_vehicle,max_speed')
->select();
$result['perimeter_count'] = count($perimeters);
$result['perimeters_with_rules'] = [];
foreach ($perimeters as $p) {
// 统计有规则设置的周界
if ($p['is_stop'] == 1 || $p['is_ban'] == 1 || $p['is_hazard'] == 1 ||
$p['max_speed'] > 0 || $p['stop_vehicle'] > 0 || $p['feasible_vehicle'] > 0) {
$result['perimeters_with_rules'][] = [
'id' => $p['id'],
'name' => $p['name'],
'is_stop' => $p['is_stop'],
'is_ban' => $p['is_ban'],
'is_hazard' => $p['is_hazard'],
'max_speed' => $p['max_speed'],
'stop_vehicle' => $p['stop_vehicle'],
'feasible_vehicle' => $p['feasible_vehicle'],
];
}
}
// 2. 查询最近同步的车辆
$vehicles = Db::name('vehicle')
->where('is_del', 1)
->field('vehicleNo,longitude,latitude,vec1,positionTime')
->order('positionTime desc')
->limit(10)
->select();
$result['recent_vehicles'] = $vehicles;
// 3. 查询今天的报警
$todayAlarms = Db::name('call_the_police')
->where('is_del', 1)
->whereTime('create_time', 'd')
->field('license,name,type,create_time')
->order('create_time desc')
->limit(20)
->select();
$result['today_alarms'] = $todayAlarms;
$result['today_alarm_count'] = count($todayAlarms);
// 4. 测试一辆车是否在某个周界内
if (!empty($vehicles) && !empty($perimeters)) {
$testVehicle = $vehicles[0];
$point = [
'lng' => $testVehicle['longitude'],
'lat' => $testVehicle['latitude']
];
$result['test_vehicle'] = $testVehicle['vehicleNo'];
$result['test_in_perimeters'] = [];
foreach ($perimeters as $p) {
$polygon = $this->parsePerimeterCoords($p['info']);
if (!empty($polygon) && $this->is_point_in_polygon($point, $polygon)) {
$result['test_in_perimeters'][] = [
'id' => $p['id'],
'name' => $p['name']
];
}
}
}
$this->success('测试结果', $result);
}
/**
* 手动创建测试报警
* 用于验证滚动报警显示功能
*/
public function create_test_alarm()
{
$now = date('Y-m-d H:i:s');
// 获取一辆最近的车辆
$vehicle = Db::name('vehicle')
->where('is_del', 1)
->field('vehicleNo,longitude,latitude')
->order('positionTime desc')
->find();
if (!$vehicle) {
$this->error('没有找到车辆数据');
}
// 创建一条测试报警(只使用存在的字段)
$alarmData = [
'name' => '测试报警-超速行驶(限速80km/h,实际100km/h)',
'type' => 1, // 超速
'license' => $vehicle['vehicleNo'],
'perimeter_point' => $vehicle['longitude'] . ',' . $vehicle['latitude'],
'trigger_type' => 1, // 自动触发
'is_del' => 1,
'create_time' => $now,
'res_department' => 1,
];
$id = Db::name('call_the_police')->insertGetId($alarmData);
$this->success('测试报警创建成功', [
'alarm_id' => $id,
'vehicle' => $vehicle['vehicleNo'],
'create_time' => $now
]);
}
/**
* 计算已上报和未上报车辆数量
* 已上报长寿API车辆在司机端有上报记录运单表或入园表
* 未上报长寿API车辆总数 - 已上报数
*/
public function get_report_count()
{
$input = file_get_contents('php://input');
$body = json_decode($input, true);
// 获取长寿API传过来的车牌列表
$apiVehicles = [];
if (!empty($body['vehicles']) && is_array($body['vehicles'])) {
$apiVehicles = $body['vehicles'];
}
$totalCount = count($apiVehicles);
$reportCount = 0;
if ($totalCount > 0) {
// 获取今天的日期范围
$todayStart = date('Y-m-d 00:00:00');
$todayEnd = date('Y-m-d 23:59:59');
// 查询今天运单表中已上报的车牌tow_license 是牵引车牌号)
$waybillVehicles = Db::name('waybill')
->where('create_time', '>=', $todayStart)
->where('create_time', '<=', $todayEnd)
->column('tow_license');
// 查询今天入园表中已上报的车牌tractor_license 是牵引车牌号)
// 注意Park模型对应的表名是 not_waybill_report
$parkVehicles = Db::name('not_waybill_report')
->where('create_time', '>=', $todayStart)
->where('create_time', '<=', $todayEnd)
->column('tractor_license');
// 合并去重已上报的车牌
$reportedVehicles = array_unique(array_merge($waybillVehicles, $parkVehicles));
// 计算API车辆中有多少已上报
foreach ($apiVehicles as $vehNo) {
if (in_array($vehNo, $reportedVehicles)) {
$reportCount++;
}
}
}
$noReportCount = $totalCount - $reportCount;
$this->success('获取成功', [
'total_count' => $totalCount,
'report_count' => $reportCount,
'no_report_count' => $noReportCount
]);
}
/**
* 根据车牌号列表获取车辆的车辆码颜色qr_color
* 用于前端统计各颜色码的车辆数量
*/
public function get_vehicle_code_colors()
{
$input = file_get_contents('php://input');
$body = json_decode($input, true);
// 获取车牌号列表
$vehicleNos = [];
if (!empty($body['vehicles']) && is_array($body['vehicles'])) {
$vehicleNos = array_unique($body['vehicles']); // 去重
}
$vehicleCodeMap = [];
if (!empty($vehicleNos)) {
// 批量查询车辆的 qr_color车辆码颜色
$vehicles = Db::name('vehicle')
->where('vehicleNo', 'in', $vehicleNos)
->where('is_del', 'in', '1,2')
->field('vehicleNo, qr_color')
->select();
// 构建车牌号到车辆码颜色的映射
foreach ($vehicles as $vehicle) {
$vehicleCodeMap[$vehicle['vehicleNo']] = intval($vehicle['qr_color']); // 1=绿码, 2=黄码, 3=红码
}
// 对于没有在数据库中找到的车辆,默认设置为 2黄码
foreach ($vehicleNos as $vehNo) {
if (!isset($vehicleCodeMap[$vehNo])) {
$vehicleCodeMap[$vehNo] = 2; // 默认黄码
}
}
}
$this->success('获取成功', $vehicleCodeMap);
}
/**
* //TODO 判断一个坐标是否在一个多边形内(由多个坐标围成的)
* //TODO 基本思想是利用射线法,计算射线与多边形各边的交点,如果是偶数,则点在多边形外,否则
* //TODO 在多边形内。还会考虑一些特殊情况,如点在多边形顶点上,点在多边形边上等特殊情况。
* @param $point //指定点坐标
* @param $pts //多边形坐标,顺时针方向
* @return bool
*/
public function is_point_in_polygon($point, $pts) {
$N = count($pts);
$boundOrVertex = true; //如果点位于多边形的顶点或边上也算做点在多边形内直接返回true
$intersectCount = 0;//cross points count of x
$precision = 2e-10; //浮点类型计算时候与0比较时候的容差
$p1 = 0;//neighbour bound vertices
$p2 = 0;
$p = $point; //测试点
$p1 = $pts[0];//left vertex
for ($i = 1; $i <= $N; ++$i) {//check all rays
// dump($p1);
if ($p['lng'] == $p1['lng'] && $p['lat'] == $p1['lat']) {
return $boundOrVertex;//p is an vertex
}
$p2 = $pts[$i % $N];//right vertex
if ($p['lat'] < min($p1['lat'], $p2['lat']) || $p['lat'] > max($p1['lat'], $p2['lat'])) {//ray is outside of our interests
$p1 = $p2;
continue;//next ray left point
}
if ($p['lat'] > min($p1['lat'], $p2['lat']) && $p['lat'] < max($p1['lat'], $p2['lat'])) {//ray is crossing over by the algorithm (common part of)
if($p['lng'] <= max($p1['lng'], $p2['lng'])){//x is before of ray
if ($p1['lat'] == $p2['lat'] && $p['lng'] >= min($p1['lng'], $p2['lng'])) {//overlies on a horizontal ray
return $boundOrVertex;
}
if ($p1['lng'] == $p2['lng']) {//ray is vertical
if ($p1['lng'] == $p['lng']) {//overlies on a vertical ray
return $boundOrVertex;
} else {//before ray
++$intersectCount;
}
} else {//cross point on the left side
$xinters = ($p['lat'] - $p1['lat']) * ($p2['lng'] - $p1['lng']) / ($p2['lat'] - $p1['lat']) + $p1['lng'];//cross point of lng
if (abs($p['lng'] - $xinters) < $precision) {//overlies on a ray
return $boundOrVertex;
}
if ($p['lng'] < $xinters) {//before ray
++$intersectCount;
}
}
}
} else {//special case when ray is crossing through the vertex
if ($p['lat'] == $p2['lat'] && $p['lng'] <= $p2['lng']) {//p crossing over p2
$p3 = $pts[($i+1) % $N]; //next vertex
if ($p['lat'] >= min($p1['lat'], $p3['lat']) && $p['lat'] <= max($p1['lat'], $p3['lat'])) { //p.lat lies between p1.lat & p3.lat
++$intersectCount;
} else {
$intersectCount += 2;
}
}
}
$p1 = $p2;//next ray left point
}
if ($intersectCount % 2 == 0) {//偶数在多边形外
return false;
} else { //奇数在多边形内
return true;
}
}
/**
* 保存车辆轨迹数据到 vehicle_line
* @param array $data 车辆数据
* @param string $now 当前时间
* @return array 返回保存结果 ['success' => 成功数量, 'fail' => 失败数量, 'errors' => 错误信息]
*/
private function saveVehicleTrajectory($data, $now) {
$result = ['success' => 0, 'fail' => 0, 'errors' => []];
// 验证数据格式
if (empty($data) || !is_array($data)) {
$result['errors'][] = '轨迹数据为空或格式错误';
return $result;
}
// 处理车辆数据列表
$vehicleList = [];
foreach ($data as $item) {
// 兼容两种数据格式:
// 格式1: $item['posList'] 包含车辆数组
// 格式2: $item 直接就是车辆数据(有 vehNo, lon, lat 字段)
if (isset($item['posList']) && is_array($item['posList'])) {
// 格式1: 有 posList 字段
foreach ($item['posList'] as $vehicle) {
$vehicleList[] = $vehicle;
}
} elseif (isset($item['vehNo']) && isset($item['lon']) && isset($item['lat'])) {
// 格式2: 直接是车辆数据
$vehicleList[] = $item;
}
}
// 处理所有车辆数据
foreach ($vehicleList as $vehicle) {
try {
// 验证必要字段
if (empty($vehicle['vehNo']) || empty($vehicle['lon']) || empty($vehicle['lat'])) {
$result['fail']++;
$result['errors'][] = '车辆数据缺少必要字段: ' . json_encode($vehicle, JSON_UNESCAPED_UNICODE);
continue;
}
$vehNo = trim($vehicle['vehNo']);
$longitude = number_format(floatval($vehicle['lon']), 7);
$latitude = number_format(floatval($vehicle['lat']), 7);
// 验证坐标有效性
if ($longitude == 0 || $latitude == 0) {
$result['fail']++;
$result['errors'][] = "车辆 {$vehNo} 坐标无效: {$longitude}, {$latitude}";
continue;
}
$coordinate = $longitude . ',' . $latitude;
// 处理定位时间
$positionTime = $now;
if (isset($vehicle['posTime'])) {
if (is_numeric($vehicle['posTime'])) {
// 处理时间戳如果是11位毫秒转换为10位
$timestamp = $vehicle['posTime'];
if ($timestamp > 9999999999) {
$timestamp = intval($timestamp / 1000); // 毫秒转秒
}
// 验证时间戳是否合理1970-2100之间
if ($timestamp > 0 && $timestamp < 4102444800) {
$positionTime = date('Y-m-d H:i:s', $timestamp);
} else {
// 时间戳异常,使用当前时间
$positionTime = $now;
}
} else {
$positionTime = $vehicle['posTime'];
}
}
// 查询当天是否已有该车辆的轨迹记录
// 使用更精确的日期查询,避免时区问题
$todayStart = date('Y-m-d 00:00:00');
$todayEnd = date('Y-m-d 23:59:59');
$lineRecord = Db::name('vehicle_line')
->where('vehicleNo', $vehNo)
->where('create_time', '>=', $todayStart)
->where('create_time', '<=', $todayEnd)
->where('is_del', 1)
->order('id desc')
->find();
// 获取速度信息(用于统计行驶/停止状态)
$speed = isset($vehicle['v1']) ? floatval($vehicle['v1']) : 0;
// 构建新的坐标点
$newPoint = [
'coordinate' => $coordinate,
'create_time' => $positionTime,
'speed' => $speed, // 保存轨迹点当时的速度
'last_coordinate_status' => 1, // 1:未处理, 2:已处理
'max_enter_status' => 1, // 1:未处理, 2:已处理
'max_out_status' => 1, // 1:未处理, 2:已处理
];
if ($lineRecord) {
// 更新现有记录,追加新坐标点
$lineData = json_decode($lineRecord['line'], true);
if (!is_array($lineData)) {
$lineData = [];
}
// 检查是否已存在相同坐标点(避免重复)
$exists = false;
foreach ($lineData as $point) {
if (isset($point['coordinate']) && $point['coordinate'] === $coordinate) {
$exists = true;
break;
}
}
if (!$exists) {
$lineData[] = $newPoint;
$updateResult = Db::name('vehicle_line')
->where('id', $lineRecord['id'])
->update(['line' => json_encode($lineData, JSON_UNESCAPED_UNICODE)]);
if ($updateResult !== false) {
$result['success']++;
} else {
$result['fail']++;
$result['errors'][] = "更新车辆 {$vehNo} 轨迹失败";
}
} else {
// 坐标点已存在,不算失败也不算成功
}
} else {
// 创建新记录前,再次检查(防止并发插入)
$checkRecord = Db::name('vehicle_line')
->where('vehicleNo', $vehNo)
->where('create_time', '>=', $todayStart)
->where('create_time', '<=', $todayEnd)
->where('is_del', 1)
->order('id desc')
->find();
if ($checkRecord) {
// 如果找到了记录,说明是并发插入,更新这条记录
$lineData = json_decode($checkRecord['line'], true);
if (!is_array($lineData)) {
$lineData = [];
}
// 检查是否已存在相同坐标点
$exists = false;
foreach ($lineData as $point) {
if (isset($point['coordinate']) && $point['coordinate'] === $coordinate) {
$exists = true;
break;
}
}
if (!$exists) {
$lineData[] = $newPoint;
$updateResult = Db::name('vehicle_line')
->where('id', $checkRecord['id'])
->update(['line' => json_encode($lineData, JSON_UNESCAPED_UNICODE)]);
if ($updateResult !== false) {
$result['success']++;
} else {
$result['fail']++;
$result['errors'][] = "更新车辆 {$vehNo} 轨迹失败(并发)";
}
}
} else {
// 确实没有记录,创建新记录
$lineData = [$newPoint];
$insertResult = Db::name('vehicle_line')->insert([
'vehicleNo' => $vehNo,
'line' => json_encode($lineData, JSON_UNESCAPED_UNICODE),
'create_time' => date('Y-m-d H:i:s'),
'is_del' => 1,
]);
if ($insertResult !== false) {
$result['success']++;
} else {
$result['fail']++;
$result['errors'][] = "插入车辆 {$vehNo} 轨迹失败";
}
}
}
} catch (\Exception $e) {
$result['fail']++;
$result['errors'][] = "保存车辆轨迹异常: " . $e->getMessage() . " - 车辆: " . (isset($vehicle['vehNo']) ? $vehicle['vehNo'] : '未知');
// 记录详细错误日志
\think\Log::error('保存车辆轨迹异常: ' . $e->getMessage() . PHP_EOL . $e->getTraceAsString());
}
}
// 如果有错误,记录日志
if ($result['fail'] > 0 && !empty($result['errors'])) {
$logData = [
'success' => $result['success'],
'fail' => $result['fail'],
'errors' => array_slice($result['errors'], 0, 10) // 只记录前10个错误
];
\think\Log::warning('车辆轨迹保存部分失败: ' . json_encode($logData, JSON_UNESCAPED_UNICODE));
}
return $result;
}
}