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; } }