vine_liutk 2023-08-30 09:50:31 +08:00
commit b8dc7bb497
50 changed files with 3554 additions and 334 deletions

View File

@ -0,0 +1,132 @@
<?php
namespace App\Console\Commands\BiAng;
use App\Enums\DeviceType;
use App\Models\Device;
use App\Models\InsecticidalLampDailyReport;
use App\Models\InsecticidalLampReport;
use App\Models\MeteorologicalMonitoringDailyLog;
use App\Models\MeteorologicalMonitoringLog;
use App\Models\SoilMonitoringDailyLog;
use App\Models\SoilMonitoringLog;
use App\Services\BiAngDeviceService;
use Illuminate\Console\Command;
class DeviceLogDailyReportCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'biang:device-log-daily-report
{--sleep=300 : 监控报告生产后的休眠时间(秒)}';
/**
* The console command description.
*
* @var string
*/
protected $description = '按设备厂商生成监控报告';
/**
* @var \App\Services\DeviceLogService
*/
protected $deviceLogService;
/**
* Execute the console command.
*/
public function handle()
{
$sleep = (int) value(fn ($sleep) => is_numeric($sleep) ? $sleep : 300, $this->option('sleep'));
while (true) {
/** @var \Illuminate\Database\Eloquent\Collection */
$devices = Device::supplierBy("device-supplier-biang")->get();
foreach ($devices as $device) {
$this->createReport($device);
}
sleep($sleep);
};
}
protected function createReport(Device $device): void
{
[$lastReportedAt, $latestReportedAt] = value(function (Device $device) {
$lastReportedAt = null;
$latestReportedAt = null;
switch ($device->type) {
case DeviceType::Meteorological:
$lastReportedAt = MeteorologicalMonitoringDailyLog::where('device_id', $device->id)
->latest('monitored_at')
->value('monitored_at');
$lastReportedAt ??= MeteorologicalMonitoringLog::where('device_id', $device->id)
->oldest('monitored_at')
->value('monitored_at');
if ($lastReportedAt) {
$latestReportedAt = MeteorologicalMonitoringLog::where('device_id', $device->id)
->latest('monitored_at')
->value('monitored_at');
}
break;
case DeviceType::Soil:
$lastReportedAt = SoilMonitoringDailyLog::where('device_id', $device->id)
->latest('monitored_at')
->value('monitored_at');
$lastReportedAt ??= SoilMonitoringLog::where('device_id', $device->id)
->oldest('monitored_at')
->value('monitored_at');
if ($lastReportedAt) {
$latestReportedAt = SoilMonitoringLog::where('device_id', $device->id)
->latest('monitored_at')
->value('monitored_at');
}
break;
case DeviceType::InsecticidalLamp:
$lastReportedAt = InsecticidalLampDailyReport::where('device_id', $device->id)
->latest('reported_at')
->value('reported_at');
$lastReportedAt ??= InsecticidalLampReport::where('device_id', $device->id)
->oldest('reported_at')
->value('reported_at');
if ($lastReportedAt) {
$latestReportedAt = InsecticidalLampReport::where('device_id', $device->id)
->latest('reported_at')
->value('reported_at');
}
break;
}
return [$lastReportedAt, $latestReportedAt];
}, $device);
if ($lastReportedAt === null || $latestReportedAt === null) {
return;
}
$service = new BiAngDeviceService();
/** @var \Carbon\Carbon */
$startAt = $lastReportedAt->copy()->startOfDay();
do {
$service->createDailyReport($device, $startAt->copy());
$startAt->addDay();
} while ($latestReportedAt->gte($startAt));
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Console\Commands\BiAng;
use App\Enums\DeviceType;
use App\Models\Device;
use App\Models\DeviceLog;
use App\Models\InsecticidalLampReport;
use App\Models\MeteorologicalMonitoringLog;
use App\Models\SoilMonitoringLog;
use App\Services\BiAngDeviceService;
use Illuminate\Console\Command;
class DeviceLogReportCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'biang:device-log-report
{--sleep=300 : 监控报告生产后的休眠时间(秒)}';
/**
* The console command description.
*
* @var string
*/
protected $description = '按设备厂商生成监控报告';
/**
* @var \App\Services\DeviceLogService
*/
protected $deviceLogService;
/**
* Execute the console command.
*/
public function handle()
{
$sleep = (int) value(fn ($sleep) => is_numeric($sleep) ? $sleep : 300, $this->option('sleep'));
while (true) {
/** @var \Illuminate\Database\Eloquent\Collection */
$devices = Device::supplierBy("device-supplier-biang")->get();
foreach ($devices as $device) {
$this->createReport($device);
}
sleep($sleep);
};
}
/**
* 创建比昂设备报告
*/
protected function createReport(Device $device): void
{
$lastReportedAt = match ($device->type) {
DeviceType::Soil => SoilMonitoringLog::where('device_id', $device->id)->latest('monitored_at')->value('monitored_at'),
DeviceType::Meteorological => MeteorologicalMonitoringLog::where('device_id', $device->id)->latest('monitored_at')->value('monitored_at'),
DeviceType::InsecticidalLamp => InsecticidalLampReport::where('device_id', $device->id)->latest('reported_at')->value('reported_at'),
default => null,
};
if (is_null($lastReportedAt ??= DeviceLog::where('device_id', $device->id)->oldest('reported_at')->value('reported_at'))) {
return;
}
if (is_null($latestReportedAt = DeviceLog::where('device_id', $device->id)->latest('reported_at')->value('reported_at'))) {
return;
}
$service = new BiAngDeviceService();
/** @var \Carbon\Carbon */
$startAt = $lastReportedAt->copy()->startOfHour();
do {
$service->createReport($device, $startAt->copy());
$startAt->addHour();
} while ($latestReportedAt->gte($startAt));
}
}

View File

@ -0,0 +1,182 @@
<?php
namespace App\Console\Commands\BiAng;
use App\Enums\DeviceStatus;
use App\Enums\DeviceType;
use App\Iot\BiAng\HttpClient;
use App\Models\Device;
use App\Models\DeviceLog;
use App\Models\InsecticidalLampReport;
use App\Models\WormPhoto;
use Illuminate\Console\Command;
use Illuminate\Support\Arr;
use RuntimeException;
use Throwable;
class DeviceLogSyncCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'biang:device-log-sync
{--sleep=60 : 设备数据同步完成后的休眠时间(秒)}';
/**
* The console command description.
*
* @var string
*/
protected $description = '按设备厂商同步数据';
/**
* Execute the console command.
*/
public function handle()
{
$sleep = (int) value(fn ($sleep) => is_numeric($sleep) ? $sleep : 60, $this->option('sleep'));
while (true) {
$this->sync();
sleep($sleep);
};
}
/**
* 执行同步
*/
protected function sync(): void
{
$now = now();
$this->info('------------------------------------------');
$this->info('同步时间: '. $now);
/** @var \Illuminate\Database\Eloquent\Collection */
$devices = Device::with(['project'])
->supplierBy("device-supplier-biang")
->whereIn('status', [DeviceStatus::Online, DeviceStatus::Offline])
->get();
/** @var \App\Models\Device */
foreach ($devices as $device) {
if (! in_array($device->type, [
DeviceType::Soil,
DeviceType::Meteorological,
DeviceType::Worm,
DeviceType::InsectSexLure,
DeviceType::InsecticidalLamp,
])) {
continue;
}
$this->info('==========================================');
$this->info('设备编号: ' . $device->sn);
$this->info('设备名称: ' . $device->name);
$this->info('设备类型: ' . match ($device->type) {
DeviceType::Soil => '土壤设备',
DeviceType::Meteorological => '气象设备',
DeviceType::Worm => '虫情设备',
DeviceType::InsectSexLure => '昆虫性诱设备',
DeviceType::InsecticidalLamp => '杀虫灯设备',
});
try {
$httpClient = $this->buildHttpClient($device);
switch ($device->type) {
case DeviceType::Soil:
$data = $httpClient->getLatestSoilReport($device->sn);
$log = DeviceLog::firstOrCreate([
'device_id' => $device->id,
'reported_at' => $data['time'],
], [
'data' => Arr::except($data, ['deviceId', 'time']),
]);
$device->update([
'status' => $now->copy()->subMinutes(60)->lt($log->reported_at) ? DeviceStatus::Online : DeviceStatus::Offline,
]);
break;
case DeviceType::Meteorological:
$data = $httpClient->getLatestMeteorologicalReport($device->sn);
$log = DeviceLog::firstOrCreate([
'device_id' => $device->id,
'reported_at' => $data['time'],
], [
'data' => Arr::except($data, ['deviceId', 'time']),
]);
$device->update([
'status' => $now->copy()->subMinutes(60)->lt($log->reported_at) ? DeviceStatus::Online : DeviceStatus::Offline,
]);
break;
case DeviceType::InsecticidalLamp:
$data = $httpClient->getLatestLampReport($device->sn);
$log = DeviceLog::firstOrCreate([
'device_id' => $device->id,
'reported_at' => $data['time'],
], [
'data' => Arr::except($data, ['deviceId', 'time']),
]);
$device->update([
'status' => $now->copy()->subMinutes(60)->lt($log->reported_at) ? DeviceStatus::Online : DeviceStatus::Offline,
]);
break;
case DeviceType::Worm:
case DeviceType::InsectSexLure:
$data = $httpClient->getWormPhotos($device->sn, $now->copy()->subHours(24), $now);
// foreach ($data['imgUrl'] as $item) {
// WormPhoto::firstOrCreate([
// 'device_id' => $device->id,
// 'uploaded_at' => $item['time'],
// ], [
// 'url' => $item['url'],
// ]);
// }
$device->update([
'status' => count($data['imgUrl'] ?? []) > 0 ? DeviceStatus::Online : DeviceStatus::Offline,
]);
break;
}
$this->info('同步成功!');
} catch (Throwable $e) {
report($e);
$this->error('同步失败: '. $e->getMessage());
}
$this->info('==========================================');
}
$this->info('------------------------------------------');
$this->newLine();
}
/**
* 创建 HTTP 客户端
*/
public function buildHttpClient(Device $device): HttpClient
{
$config = json_decode($device->project?->value, true);
if (! is_array($config)) {
throw new RuntimeException('账户信息未找到');
}
return new HttpClient($config['username'] ?? '', $config['password'] ?? '');
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Console\Commands\BiAng;
use App\Enums\DeviceStatus;
use App\Enums\DeviceType;
use App\Models\Device;
use App\Models\WormReport;
use App\Services\BiAngDeviceService;
use Illuminate\Console\Command;
use Illuminate\Support\Arr;
class WormStatisticsSyncCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'biang:worm-statistics-sync';
/**
* The console command description.
*
* @var string
*/
protected $description = '虫情设备统计数据同步';
/**
* Execute the console command.
*/
public function handle()
{
$this->sync();
}
protected function sync(): void
{
/** @var \Illuminate\Database\Eloquent\Collection */
$devices = Device::with(['project'])
->supplierBy('device-supplier-biang')
->where('type', DeviceType::Worm)
->whereIn('status', [DeviceStatus::Online, DeviceStatus::Offline])
->get();
if ($devices->isEmpty()) {
$this->warn('没有找到虫情设备');
return;
}
$today = today();
foreach ($devices as $device) {
$this->info('==================================');
$latestReportedAt = WormReport::Where('device_id', $device->id)->latest('reported_at')->value('reported_at');
$start = $latestReportedAt ? $latestReportedAt->copy() : $today->copy()->subDays(179);
$days = $start->diffInDays($today, false);
if ($days > 179) {
$end = $start->copy()->addDays(179);
} else {
if ($days < 2) {
$start = $today->copy()->subDays(2);
}
$end = $today->copy();
}
$this->info('设备编号: '.$device->sn);
$this->info('开始日期: '.$start->toDateString());
$this->info('结束日期: '.$end->toDateString());
$statistics = (new BiAngDeviceService())->getWormStatistics($device, $start, $end);
foreach (data_get($statistics, 'data.records', []) as $item) {
$data = collect(Arr::except($item, '日期'))->map(fn ($v, $k) => ['name' => $k, 'num' => $v]);
WormReport::updateOrCreate([
'device_id' => $device->id,
'reported_at' => $item['日期'],
], [
'agricultural_base_id' => $device->agricultural_base_id,
'worm_num' => $data->sum('num'),
'data' => $data->values(),
]);
}
$this->info('==================================');
}
}
}

View File

@ -1,93 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Enums\DeviceStatus;
use App\Enums\DeviceType;
use App\Models\Device;
use App\Models\SoilMonitoringLog;
use App\Services\LinkosDeviceLogService;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Throwable;
class SoilMonitoringLogFixCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'soil-monitoring-log:fix {hour? : 时间格式 Y-m-d H:i:s, 示例: 2022-11-11 00:00:00}';
/**
* The console command description.
*
* @var string
*/
protected $description = '修复土壤监控数据';
/**
* Execute the console command.
*
* @return int
*/
public function handle(LinkosDeviceLogService $linkosDeviceLogService)
{
$time = now()->subHour()->startOfHour();
if ($hour = $this->argument('hour')) {
$time = Carbon::createFromFormat('Y-m-d H:i:s', $hour)->startOfHour();
}
$devices = Device::where('type', DeviceType::Soil)
->where('status', DeviceStatus::Online)
->get();
foreach ($devices as $device) {
$last = SoilMonitoringLog::where('device_id', $device->id)
->where('monitored_at', $time->copy()->subHour())
->first();
if ($last === null) {
continue;
}
try {
$log = SoilMonitoringLog::firstOrCreate([
'device_id' => $device->id,
'monitored_at' => $time,
], [
'agricultural_base_id' => $device->agricultural_base_id,
]);
foreach ([
'conductivity',
'humidity',
'temperature',
'n',
'p',
'k',
] as $key) {
if (is_null($log->{$key})) {
$log->{$key} = $last->{$key};
}
}
if ($log->isDirty()) {
$log->is_filled = true;
}
$log->save();
if ($log->wasChanged()) {
$linkosDeviceLogService->handleSoilMonitoringDailyLog($device, $time);
}
} catch (Throwable $e) {
report($e);
}
}
return Command::SUCCESS;
}
}

View File

@ -1,93 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Enums\DeviceStatus;
use App\Enums\DeviceType;
use App\Models\Device;
use App\Models\WaterQualityMonitoringLog;
use App\Services\LinkosDeviceLogService;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Throwable;
class WaterQualityMonitoringLogFixCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'water-quality-monitoring-log:fix {hour? : 时间格式 Y-m-d H:i:s, 示例: 2022-11-11 00:00:00}';
/**
* The console command description.
*
* @var string
*/
protected $description = '修复水质监控数据';
/**
* Execute the console command.
*
* @return int
*/
public function handle(LinkosDeviceLogService $linkosDeviceLogService)
{
$time = now()->subHour()->startOfHour();
if ($hour = $this->argument('hour')) {
$time = Carbon::createFromFormat('Y-m-d H:i:s', $hour)->startOfHour();
}
$devices = Device::where('type', DeviceType::WaterQuality)
->where('status', DeviceStatus::Online)
->get();
foreach ($devices as $device) {
$last = WaterQualityMonitoringLog::where('device_id', $device->id)
->where('monitored_at', $time->copy()->subHour())
->first();
if ($last === null) {
continue;
}
try {
$log = WaterQualityMonitoringLog::firstOrCreate([
'device_id' => $device->id,
'monitored_at' => $time,
], [
'agricultural_base_id' => $device->agricultural_base_id,
]);
foreach ([
'chlorine',
'conductivity',
'oxygen',
'ph',
'temperature',
'turbidity',
] as $key) {
if (is_null($log->{$key})) {
$log->{$key} = $last->{$key};
}
}
if ($log->isDirty()) {
$log->is_filled = true;
}
$log->save();
if ($log->wasChanged()) {
$linkosDeviceLogService->handleWaterQualityMonitoringDailyLog($device, $time);
}
} catch (Throwable $e) {
report($e);
}
}
return Command::SUCCESS;
}
}

View File

@ -15,11 +15,7 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->command(Commands\SoilMonitoringLogFixCommand::class)
->hourly()
->runInBackground();
$schedule->command(Commands\WaterQualityMonitoringLogFixCommand::class)
$schedule->command(Commands\BiAng\WormStatisticsSyncCommand::class)
->hourly()
->runInBackground();
}

View File

@ -8,6 +8,9 @@ enum DeviceType: int
case Soil = 2; // 土壤设备
case WaterQuality = 3; // 水质设备
case Meteorological = 4; // 气象设备
case Worm = 5; // 虫情设备
case InsectSexLure = 6; // 昆虫性诱设备
case InsecticidalLamp = 7; // 杀虫灯设备
/**
* @return string
@ -27,6 +30,9 @@ enum DeviceType: int
static::Soil->value => '土壤设备',
static::WaterQuality->value => '水质设备',
static::Meteorological->value => '气象设备',
static::Worm->value => '虫情设备',
static::InsectSexLure->value => '昆虫性诱设备',
static::InsecticidalLamp->value => '杀虫灯设备',
];
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Exceptions;
use RuntimeException;
class BiAngException extends RuntimeException
{
}

View File

@ -5,15 +5,15 @@ namespace App\Http\Controllers;
use App\Enums\BaseType;
use App\Enums\DeviceStatus;
use App\Enums\DeviceType;
use App\Enums\OperationType;
use App\Helpers\Paginator;
use App\Http\Requestes\AgriculturalBaseRequest;
use App\Http\Resources\AgriculturalBaseResource;
use App\Models\AgriculturalBase;
use App\Models\Device;
use App\Services\OperationLogService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Services\OperationLogService;
use App\Enums\OperationType;
class AgriculturalBaseController extends Controller
{
@ -26,7 +26,7 @@ class AgriculturalBaseController extends Controller
public function index(Request $request)
{
$query = AgriculturalBase::with(['crops','industry'])->filter($request->all())->sort();
$list = $query->paginate(Paginator::resolvePerPage('per_page', 20, 50));
$list = $query->paginate(Paginator::resolvePerPage('per_page', 20));
return $this->json(AgriculturalBaseResource::collection($list));
}

File diff suppressed because it is too large Load Diff

View File

@ -8,9 +8,9 @@ use App\Models\RiceShrimpFlow;
use App\Models\RiceShrimpIndustry;
use App\Models\RiceShrimpPrice;
use App\Models\RiceShrimpWeeklyPrice;
use Peidikeji\Keywords\Models\Keywords;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Peidikeji\Keywords\Models\Keywords;
class ChartController extends Controller
{
@ -72,7 +72,7 @@ class ChartController extends Controller
{
$now = now();
$weeks = Keywords::where('type_key', 'weeks-per-year')->pluck('name', 'id');
$weeks = Keywords::where('type_key', 'weeks-per-year')->pluck('name', 'key');
$years = RiceShrimpWeeklyPrice::select('year')
->groupBy('year')
@ -105,7 +105,7 @@ class ChartController extends Controller
$riceShrimpWeeklyPricesTable = (new RiceShrimpWeeklyPrice)->getTable();
$latestPrice = RiceShrimpWeeklyPrice::query()
->join($keywordsTable, fn ($join) => $join->on("$riceShrimpWeeklyPricesTable.week", '=', "$keywordsTable.id"))
->join($keywordsTable, fn ($join) => $join->on("$riceShrimpWeeklyPricesTable.week", '=', DB::raw("$keywordsTable.key::INTEGER")))
->where("$keywordsTable.type_key", 'weeks-per-year')
->latest("$riceShrimpWeeklyPricesTable.year")
->latest(DB::raw("$keywordsTable.key::INTEGER"))

View File

@ -4,29 +4,35 @@ namespace App\Http\Controllers;
use App\Enums\DeviceStatus;
use App\Enums\DeviceType;
use App\Enums\OperationType;
use App\Helpers\Paginator;
use App\Http\Requestes\DeviceRequest;
use App\Http\Resources\DeviceResource;
use App\Models\Device;
use App\Models\MeteorologicalMonitoringLog;
use App\Models\MeteorologicalMonitoringDailyLog;
use App\Models\SoilMonitoringLog;
use App\Models\SoilMonitoringDailyLog;
use App\Models\WaterQualityMonitoringLog;
use App\Models\WaterQualityMonitoringDailyLog;
use App\Models\AgriculturalBase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
use App\Models\Device;
use App\Models\InsecticidalLampDailyReport;
use App\Models\InsecticidalLampReport;
use App\Models\MeteorologicalMonitoringDailyLog;
use App\Models\MeteorologicalMonitoringLog;
use App\Models\SoilMonitoringDailyLog;
use App\Models\SoilMonitoringLog;
use App\Models\WaterQualityMonitoringDailyLog;
use App\Models\WaterQualityMonitoringLog;
use App\Models\WormPhoto;
use App\Models\WormReport;
use App\Services\BiAngDeviceService;
use App\Services\OperationLogService;
use App\Enums\OperationType;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Peidikeji\Setting\Models\Setting;
class DeviceController extends Controller
{
public function index(Request $request)
{
$query = Device::with('base')->filter($request->input())->orderBy('sort', 'desc');
$query = Device::with(['base', 'supplier', 'project'])->filter($request->input())->orderBy('sort', 'desc');
$list = $query->paginate(Paginator::resolvePerPage('per_page', 20, 50));
return $this->json(DeviceResource::collection($list));
@ -51,6 +57,7 @@ class DeviceController extends Controller
public function show(Device $device)
{
$device->loadMissing(['base', 'supplier', 'project']);
return $this->json(DeviceResource::make($device));
}
@ -414,48 +421,46 @@ class DeviceController extends Controller
}
}
/**
* 查询设备今天(按天),近一周(按天),近一个月(按天)
*/
public function timeZoneList(Request $request){
$deviceId = $request->input('device_id');
//不传开始时间,结束时间,则默认是查当天(按小时)
$startTime = $request->input('start_time');
$endTime = $request->input('end_time');
$diffDays = 0;
$day = date('Y-m-d');
//如果传了开始时间和结束时间,计算中间天数
if($startTime && $endTime){
if($startTime == $endTime){//查询某一天
$day = $startTime;
}else{
$startDay = Carbon::parse($startTime);
$endDay = Carbon::parse($endTime);
$diffDays = $startDay->diffInDays($endDay, false);
public function timeZoneList(Request $request)
{
$request->validate([
'device_id' => ['bail', 'required'],
'start_time' => ['bail', 'nullable', 'date_format:Y-m-d'],
'end_time' => ['bail', 'nullable', 'date_format:Y-m-d'],
]);
$isSameDay = true;
if ($request->filled('start_time') && $request->filled('end_time')) {
$startTime = Carbon::parse($request->input('start_time'))->startOfDay();
$endTime = Carbon::parse($request->input('end_time'))->startOfDay();
if ($startTime->gt($endTime)) {
throw ValidationException::withMessages([
'start_time' => ['开始时间不能大于结束时间'],
]);
}
// 如果开始时间和结束时间是同一天
if ($startTime->eq($endTime)) {
$endTime = $startTime->isToday() ? now() : $startTime->copy()->endOfDay();
} else {
$isSameDay = false;
}
} else {
$endTime = now();
$startTime = $endTime->copy()->startOfDay();
}
$xKeys = [];
if($diffDays){
for ($i = 0; $i<=$diffDays; $i++) {
$xKeys[] =(clone $startDay)->addDays($i)->startOfDay()->format('Y-m-d H:i:s');
}
}else{
//调整截至到当前小时
$h = 23;
if($day == date('Y-m-d')){
$h = date('H');
}
for ($i = 0; $i < ($h+1); $i++) {
$xKeys[] = $day.' '.str_pad($i, 2, '0', STR_PAD_LEFT).':00:00';
}
}
$device = Device::find($deviceId);
$modelQuery = null;
$getArr = [];
$device = Device::findOrFail($request->input('device_id'));
$fields = [];
$monitoringLogs = collect();
switch ($device->type) {
case DeviceType::Meteorological://气象设备
$getArr = [
// 气象设备
case DeviceType::Meteorological:
$fields = [
'wind_speed',
'wind_direction',
'wind_degree',
@ -467,17 +472,34 @@ class DeviceController extends Controller
'illumination',
'pm25',
'pm10',
'rainfall',
];
if($diffDays) {
$getArr[] = 'daily_rainfall';
$modelQuery = MeteorologicalMonitoringDailyLog::query()->whereBetween('monitored_at', [$startTime, $endTime]);
}else{
$getArr[] = 'current_rainfall';
$modelQuery = MeteorologicalMonitoringLog::query()->whereDate('monitored_at', $day);
}
/** @var \Illuminate\Support\Collection */
$monitoringLogs = (
$isSameDay
? MeteorologicalMonitoringLog::query()
: MeteorologicalMonitoringDailyLog::query()
)
->where('device_id', $device->id)
->whereBetween('monitored_at', [$startTime, $endTime])
->get()
->mapWithKeys(function ($item) use ($fields) {
$key = $item->monitored_at->toDateTimeString();
$data = collect($fields)->mapWithKeys(fn ($field) => [$field => $field !== 'rainfall' ? $item[$field] : null])->all();
if ($item instanceof MeteorologicalMonitoringDailyLog) {
$data['rainfall'] = $item->daily_rainfall;
} else {
$data['rainfall'] = $item->current_rainfall;
}
return [$key => $data];
});
break;
case DeviceType::Soil://土壤设备
$getArr = [
// 土壤设备
case DeviceType::Soil:
$fields = [
'conductivity',
'humidity',
'temperature',
@ -485,14 +507,25 @@ class DeviceController extends Controller
'p',
'k',
];
if($diffDays) {
$modelQuery = SoilMonitoringDailyLog::query()->whereBetween('monitored_at', [$startTime, $endTime]);
}else{
$modelQuery = SoilMonitoringLog::query()->whereDate('monitored_at', $day);
}
/** @var \Illuminate\Support\Collection */
$monitoringLogs = (
$isSameDay
? SoilMonitoringLog::query()
: SoilMonitoringDailyLog::query()
)
->where('device_id', $device->id)
->whereBetween('monitored_at', [$startTime, $endTime])
->get()
->mapWithKeys(function ($item) use ($fields) {
$key = $item->monitored_at->toDateTimeString();
$data = collect($fields)->mapWithKeys(fn ($field) => [$field => $item[$field]])->all();
return [$key => $data];
});
break;
case DeviceType::WaterQuality://水质设备
$getArr = [
// 水质设备
case DeviceType::WaterQuality:
$fields = [
'chlorine',
'conductivity',
'oxygen',
@ -500,62 +533,96 @@ class DeviceController extends Controller
'temperature',
'turbidity',
];
if($diffDays) {
$modelQuery = WaterQualityMonitoringDailyLog::query()->whereBetween('monitored_at', [$startTime, $endTime]);
}else{
$modelQuery = WaterQualityMonitoringLog::query()->whereDate('monitored_at', $day);
}
/** @var \Illuminate\Support\Collection */
$monitoringLogs = (
$isSameDay
? WaterQualityMonitoringLog::query()
: WaterQualityMonitoringDailyLog::query()
)
->where('device_id', $device->id)
->whereBetween('monitored_at', [$startTime, $endTime])
->get()
->mapWithKeys(function ($item) use ($fields) {
$key = $item->monitored_at->toDateTimeString();
$data = collect($fields)->mapWithKeys(fn ($field) => [$field => $item[$field]])->all();
return [$key => $data];
});
break;
// 杀虫灯
case DeviceType::InsecticidalLamp:
$fields = [
'battery_vol',
'killed_num',
'air_temperature',
'air_humidity',
'charging_vol',
'high_vol',
];
/** @var \Illuminate\Support\Collection */
$monitoringLogs = (
$isSameDay
? InsecticidalLampReport::query()
: InsecticidalLampDailyReport::query()
)
->where('device_id', $device->id)
->whereBetween('reported_at', [$startTime, $endTime])
->get()
->mapWithKeys(function ($item) use ($fields) {
$key = $item->reported_at->toDateTimeString();
$data = collect($fields)->mapWithKeys(fn ($field) => [$field => $item[$field]])->all();
return [$key => $data];
});
break;
}
if($modelQuery){
$datalist = $modelQuery->where('device_id', $deviceId)->get()->keyBy('monitored_at')->toArray();
}
$data = [];
foreach ($getArr as $column){
$data[$column] = [];
$_value = null;
foreach($xKeys as $key){
if($device->type == DeviceType::WaterQuality){//如果是水质设备,则写死假数据
switch($column){
case 'chlorine':
$data[$column][$key] = 0.016;
break;
case 'conductivity':
$data[$column][$key] = 563 ;//电导率
break;
case 'oxygen':
$data[$column][$key] = 0.09;//含氧量
break;
case 'ph':
$data[$column][$key] = rand(750, 770) / 100;
break;
case 'temperature':
$data[$column][$key] = rand(2400, 2600) / 100;
break;
case 'turbidity':
$data[$column][$key] = 0.33;
break;
return $this->json(
collect($fields)->mapWithKeys(function ($field) use ($device, $monitoringLogs, $isSameDay, $startTime, $endTime) {
$data = [];
$beginTime = $startTime->copy();
do {
$key = $beginTime->toDateTimeString();
$monitoringLog = $monitoringLogs->get($key);
if (is_null($monitoringLog)) {
// 如果是水质设备,则写死假数据
if($device->type == DeviceType::WaterQuality){
switch($field){
case 'chlorine':
$data[$key] = 0.016;
break;
case 'conductivity':
$data[$key] = rand(560, 565);//电导率
break;
case 'oxygen':
$data[$key] = 0.09;//含氧量
break;
case 'ph':
$data[$key] = rand(750, 770) / 100;
break;
case 'temperature':
$data[$key] = rand(2400, 2600) / 100;
break;
case 'turbidity':
$data[$key] = 0.33;
break;
}
} else {
$data[$key] = null;
}
} else {
$data[$key] = $monitoringLog[$field];
}
}else{
// if($datalist[$key][$column] ?? null){//如果存在数据则暂存该值
// $_value = $datalist[$key][$column];
// }
// //判断是否超过离线时间;
// if(true){//未超过, 判断和设备离线时间关系-todo
// $data[$column][$key] = $_value;
// }else{
$data[$column][$key] = $datalist[$key][$column] ?? null;
// }
}
}
}
//强制统一气象降雨量,日和天字段不统一问题
if(isset($data['daily_rainfall'])) {
$data['rainfall'] = $data['daily_rainfall'];
}elseif(isset($data['current_rainfall'])){
$data['rainfall'] = $data['current_rainfall'];
}
return $this->json($data);
$isSameDay ? $beginTime->addHours(1) : $beginTime->addDays(1);
} while ($beginTime->lte($endTime));
return [$field => $data];
})
);
}
public function getFfmpegServiceIp(){
@ -571,4 +638,116 @@ class DeviceController extends Controller
return $this->json($data);
}
/**
* 虫情统计
*/
public function wormStatics($id, Request $request)
{
$request->validate([
'start_time' => ['bail', 'required_with:end_time', 'date_format:Y-m-d'],
'end_time' => ['bail', 'required_with:start_time', 'date_format:Y-m-d'],
], [], [
'start_time' => '开始时间',
'end_time' => '结束时间',
]);
// 结束时间
$endTime = $request->whenFilled(
'end_time',
fn ($time) => Carbon::parse($time)->startOfDay(),
fn () => now()->startOfDay(),
);
// 开始时间
$startTime = $request->whenFilled(
'start_time',
fn ($time) => Carbon::parse($time)->startOfDay(),
fn () => $endTime->copy()->subDays(6),
);
if ($startTime->gt($endTime)) {
throw ValidationException::withMessages([
'start_time' => ['开始时间不能大于结束时间'],
]);
}
$device = Device::findOrFail($id);
$wormReports = WormReport::where('device_id', $device->id)
->whereBetween('reported_at', [$startTime->toDateString(), $endTime->toDateString()])
->pluck('worm_num', 'reported_at');
$data = [];
do {
$key = $startTime->toDateString();
$data[$key] = $wormReports->get($key);
$startTime->addDay();
} while ($startTime->lte($endTime));
return $this->json($data);
}
/**
* 虫情图片
*/
public function wormPhotos($id, Request $request, BiAngDeviceService $biAngDeviceService)
{
$request->validate([
'start_time' => ['bail', 'required_with:end_time', 'date_format:Y-m-d'],
'end_time' => ['bail', 'required_with:start_time', 'date_format:Y-m-d'],
], [], [
'start_time' => '开始时间',
'end_time' => '结束时间',
]);
// 结束时间
$endTime = $request->whenFilled(
'end_time',
fn ($time) => Carbon::parse($time)->endOfDay(),
fn () => now()->endOfDay(),
);
// 开始时间
$startTime = $request->whenFilled(
'start_time',
fn ($time) => Carbon::parse($time)->startOfDay(),
fn () => $endTime->copy()->startOfDay(),
);
if ($startTime->gt($endTime)) {
throw ValidationException::withMessages([
'start_time' => ['开始时间不能大于结束时间'],
]);
}
$device = Device::findOrFail($id);
$data = [];
switch ($device->supplier_key) {
case 'device-supplier-biang':
$result = $biAngDeviceService->getWormPhotos(
$device,
Carbon::parse($request->input('start_time')),
Carbon::parse($request->input('end_time')),
);
$data = $result['imgUrl'];
break;
default:
$wormPhotos = WormPhoto::where('device_id', $device->id)
->whereBetween('uploaded_at', [$startTime, $endTime])
->latest('uploaded_at')
->get();
$data = $wormPhotos->map(fn ($item) => [
'id' => $item->id,
'url' => $item->url,
'time' => $item->created_at->toDateTimeString(),
]);
break;
}
return $this->json($data);
}
}

View File

@ -10,6 +10,8 @@ class WeeksPerYearController extends Controller
{
public function __invoke(Request $request)
{
return Keywords::select('name', 'id')->where('type_key', 'weeks-per-year')->get();
$keywords = Keywords::where('type_key', 'weeks-per-year')->pluck('name', 'key');
return $keywords->map(fn ($name, $key) => ['id' => $key, 'name' => $name])->values();
}
}

View File

@ -22,6 +22,7 @@ class DeviceRequest extends FormRequest
'agricultural_base_id' => 'required|integer|min:0',
'sn' => 'required|string|max:64',
'monitoring_point' => 'required|string|max:100',
'supplier_key' => 'required',
'extends' => 'required_if:type,1',
'extends.ip' => 'required_if:type,1|string',
'extends.port' => 'required_if:type,1|string',
@ -34,6 +35,13 @@ class DeviceRequest extends FormRequest
];
}
public function attributes()
{
return [
'supplier_key' => '设备厂商',
];
}
public function messages()
{
$messages = [

View File

@ -22,7 +22,7 @@ class RiceShrimpWeeklyPriceStoreRequest extends FormRequest
'week' => [
'required',
'int',
Rule::exists(Keywords::class, 'id')->where(function ($query) {
Rule::exists(Keywords::class, 'key')->where(function ($query) {
return $query->where('type_key', 'weeks-per-year');
}),
],

View File

@ -20,7 +20,7 @@ class RiceShrimpWeeklyPriceUpdateRequest extends FormRequest
'week' => [
'filled',
'int',
Rule::exists(Keywords::class, 'id')->where(function ($query) {
Rule::exists(Keywords::class, 'key')->where(function ($query) {
return $query->where('type_key', 'weeks-per-year');
}),
],

View File

@ -31,6 +31,18 @@ class DeviceResource extends JsonResource
'created_at' => strtotime($this->created_at) ?? 0, //录入时间
'is_recommend' => $this->is_recommend,
'sort' => $this->sort ?? 0,
'supplier' => $this->whenLoaded('supplier', function () {
return $this->supplier ? [
'id' => $this->supplier->key,
'name' => $this->supplier->name,
] : null;
}),
'project' => $this->whenLoaded('project', function () {
return $this->project ? [
'id' => $this->project->key,
'name' => $this->project->name,
] : null;
}),
];
}
}

View File

@ -0,0 +1,167 @@
<?php
namespace App\Iot\BiAng;
use App\Exceptions\BiAngException;
use Carbon\Carbon;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use RuntimeException;
class HttpClient
{
public function __construct(
protected readonly string $username,
protected readonly string $password,
) {
}
/**
* 获取最新的土壤数据
*/
public function getLatestSoilReport(string $deviceId)
{
$result = $this->get(
$this->apiUrl('/api/open-api/open/soilMoisture/getCurrentDeviceData'),
[
'deviceId' => $deviceId,
]
);
return $result['data'];
}
/**
* 获取最新的气象数据
*/
public function getLatestMeteorologicalReport(string $deviceId)
{
$result = $this->get(
$this->apiUrl('/api/open-api/open/weather/getCurrentDeviceData'),
[
'deviceId' => $deviceId,
]
);
return $result['data'];
}
/**
* 获取最新的杀虫灯数据
*/
public function getLatestLampReport(string $deviceId)
{
$result = $this->get(
$this->apiUrl2('/open-api/open/getCurrentLampData'),
[
'deviceId' => $deviceId,
]
);
return $result['data'];
}
/**
* 虫情设备/昆虫性诱设备 - 查询某个时间段内的图片
*/
public function getWormPhotos(string $deviceId, Carbon $start, Carbon $end)
{
$result = $this->get(
$this->apiUrl('/api/open-api/open/getDevicePhotos'),
[
'deviceId' => $deviceId,
'startTime' => $start->toDateString(),
'endTime' => $end->toDateString(),
]
);
return $result['data'];
}
/**
* 虫情设备 - (识别款)图片虫数识别统计
*/
public function getWormStatistics(string $deviceId, Carbon $start, Carbon $end)
{
$result = $this->get(
$this->apiUrl('/api/open-api/open/getAllStatistics'),
[
'imei' => $deviceId,
'startAt' => $start->toDateString(),
'endAt' => $end->toDateString(),
]
);
return $result['data'];
}
public function get(string $url, array $query = []): array
{
return $this->request('GET', $url, [
'query' => $query,
]);
}
/**
* @param string $url
* @param array $data
* @return array
*/
public function post(string $url, array $data = []): array
{
return $this->request('POST', $url, [
'json' => $data,
]);
}
/**
* @param string $method
* @param string $url
* @param array $options
* @return array
*
* @throws \Illuminate\Http\Client\RequestException
* @throws \RuntimeException
*/
public function request(string $method, string $url, array $options = []): array
{
switch (strtoupper($method)) {
case 'GET':
$options['query'] = array_merge($options['query'], [
'username' => $this->username,
'password' => $this->password,
]);
break;
case 'POST':
$options['json'] = array_merge($options['json'], [
'username' => $this->username,
'password' => $this->password,
]);
break;
}
/** @var \Illuminate\Http\Client\Response */
$response = Http::withHeaders([
'Content-Type' => 'application/json',
])->send($method, $url, $options);
$result = $response->throw()->json();
if (data_get($result, 'code') === 200) {
return $result;
}
throw new BiAngException($result['code'].':'.($result['msg']??'出错啦!'), 500);
}
protected function apiUrl(string $path): string
{
return 'http://yun.bigdata5s.com'.Str::start($path, '/');
}
protected function apiUrl2(string $path): string
{
return 'http://yun-api.bigdata5s.com'.Str::start($path, '/');
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace App\Iot\Linkos;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class HttpClient
{
public const ENDPOINT_URL = 'http://service.easylinkin.com';
public function __construct(
protected readonly string $apiKey,
protected readonly string $apiSecret,
) {
}
/**
* 获取设备的历史数据
*
* @param string $deviceId
* @param \Illuminate\Support\Carbon $start
* @param \Illuminate\Support\Carbon $end
* @param int $page
* @param int $perPage
* @return array
*/
public function deviceFlowList(string $deviceId, Carbon $start, Carbon $end, int $page = 1, int $perPage = 50): array
{
$result = $this->post('/api/deviceFlow/v1/list', [
'device_id' => $deviceId,
'start_time' => $start->unix() * 1000,
'end_time' => $end->unix() * 1000,
'pageable' => [
'page' => $page - 1,
'size' => $perPage,
],
]);
if (data_get($result, 'success') !== true) {
throw new RuntimeException(data_get($result, 'msg', '出错啦!'));
}
return $result['data'];
}
/**
* 设备数据下行
*
* @param string $deviceId
* @param string $service
* @param array $data
* @param boolean $confirm
* @param boolean $clear
* @param boolean $schedule
* @return array
*/
public function deviceDataDownlink(string $deviceId, string $service, array $data = [], bool $confirm = true, bool $clear = true, bool $schedule = false): array
{
return $this->post('/api/down', [
'device_id' => $deviceId,
'service_id' => $service,
'parameter' => $data,
'clear' => (int) $clear,
'schedule' => (int) $schedule,
'confirm' => (int) $confirm,
]);
}
/**
* 获取设备最新属性数据
*/
public function getDeviceStatus(string $deviceId, array $props): array
{
$result = $this->get('/api/deviceStatus/v1/getDeviceStatus', [
'deviceCode' => $deviceId,
'prop' => implode(",", $props),
]);
if (data_get($result, 'success') !== true) {
throw new RuntimeException(data_get($result, 'msg', '出错啦!'));
}
return $result['data'];
}
public function get(string $url, array $query = []): array
{
return $this->request('GET', $url, [
'query' => $query,
]);
}
/**
* @param string $url
* @param array $data
* @return array
*/
public function post(string $url, array $data = []): array
{
return $this->request('POST', $url, [
'json' => $data,
]);
}
/**
* @param string $method
* @param string $url
* @param array $options
* @return array
*
* @throws \Illuminate\Http\Client\RequestException
* @throws \RuntimeException
*/
public function request(string $method, string $url, array $options = []): array
{
$nonce = $this->nonce();
$timestamp = now()->getTimestampMs();
/** @var \Illuminate\Http\Client\Response */
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'api-key' => $this->apiKey,
'Nonce' => $nonce,
'Timestamp' => $timestamp,
'Signature' => $this->sign(compact('nonce', 'timestamp')),
])->baseUrl(self::ENDPOINT_URL)->send($method, $url, $options);
return $response->throw()->json();
}
/**
* @param array $data
* @return string
*/
protected function sign(array $data): string
{
return sha1(
sprintf(
'%s%s%s',
$data['nonce'] ?? '',
$data['timestamp'] ?? '',
$this->apiSecret
)
);
}
protected function nonce(): string
{
$nonce = '';
for ($i = 0; $i < 8; $i++) {
$nonce .= mt_rand(0, 9);
}
return $nonce;
}
}

View File

@ -6,6 +6,11 @@ use EloquentFilter\ModelFilter;
class DeviceFilter extends ModelFilter
{
public function sn($sn)
{
return $this->where('sn', $sn);
}
public function point($point)
{
return $this->where('monitoring_point', 'like', $point.'%');
@ -26,6 +31,16 @@ class DeviceFilter extends ModelFilter
return $this->where('status', $status);
}
public function supplierKey($supplierKey)
{
return $this->where('supplier_key', $supplierKey);
}
public function projectKey($projectKey)
{
return $this->where('project_key', $projectKey);
}
public function isRecommend($isRecommend){
return $this->where('is_recommend', $isRecommend);
}

View File

@ -2,13 +2,15 @@
namespace App\Models;
use App\Enums\DeviceType;
use App\Enums\DeviceStatus;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Model;
use App\Enums\DeviceType;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use EloquentFilter\Filterable;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Peidikeji\Keywords\Models\Keywords;
class Device extends Model
{
@ -37,8 +39,15 @@ class Device extends Model
'created_by',
'updated_by',
'sort',
'supplier_key',
'project_key',
];
public function scopeSupplierBy(Builder $query, string $supplier): void
{
$query->whereHas('supplier', fn ($query) => $query->where('supplier_key', $supplier));
}
public function base()
{
return $this->belongsTo(AgriculturalBase::class, 'agricultural_base_id');
@ -53,4 +62,24 @@ class Device extends Model
{
return $this->belongsTo(AdminUser::class, 'updated_by');
}
public function supplier(): BelongsTo
{
return $this->belongsTo(Keywords::class, 'supplier_key', 'key');
}
public function project(): BelongsTo
{
return $this->belongsTo(Keywords::class, 'project_key', 'key');
}
public function isTypeSoil(): bool
{
return $this->type === DeviceType::Soil;
}
public function isTypeMeteorological(): bool
{
return $this->type === DeviceType::Meteorological;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class DeviceLog extends Model
{
use HasFactory;
protected $casts = [
'data' => 'json',
'reported_at' => 'datetime',
];
protected $fillable = [
'device_id',
'data',
'reported_at',
];
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class InsecticidalLampDailyReport extends Model
{
use HasFactory;
protected $casts = [
'reported_at' => 'date',
];
protected $fillable = [
'device_id',
'agricultural_base_id',
'battery_vol',
'killed_num',
'air_temperature',
'air_humidity',
'charging_vol',
'high_vol',
'reported_at',
];
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class InsecticidalLampReport extends Model
{
use HasFactory;
protected $casts = [
'reported_at' => 'datetime',
];
protected $fillable = [
'device_id',
'agricultural_base_id',
'battery_vol',
'killed_num',
'air_temperature',
'air_humidity',
'charging_vol',
'high_vol',
'reported_at',
];
}

View File

@ -10,6 +10,15 @@ class MeteorologicalMonitoringDailyLog extends Model
{
use HasFactory;
const WIND_DIRECTION_NORTH = 0;
const WIND_DIRECTION_NORTHEAST = 1;
const WIND_DIRECTION_EAST = 2;
const WIND_DIRECTION_SOUTHEAST = 3;
const WIND_DIRECTION_SOUTH = 4;
const WIND_DIRECTION_SOUTHWEST = 5;
const WIND_DIRECTION_WEST = 6;
const WIND_DIRECTION_NORTHWEST = 7;
protected $casts = [
'wind_direction' => WindDirection::class,
'monitored_at' => 'date',

View File

@ -10,6 +10,15 @@ class MeteorologicalMonitoringLog extends Model
{
use HasFactory;
const WIND_DIRECTION_NORTH = 0;
const WIND_DIRECTION_NORTHEAST = 1;
const WIND_DIRECTION_EAST = 2;
const WIND_DIRECTION_SOUTHEAST = 3;
const WIND_DIRECTION_SOUTH = 4;
const WIND_DIRECTION_SOUTHWEST = 5;
const WIND_DIRECTION_WEST = 6;
const WIND_DIRECTION_NORTHWEST = 7;
protected $casts = [
'wind_direction' => WindDirection::class,
'monitored_at' => 'datetime',

View File

@ -17,7 +17,7 @@ class RiceShrimpWeeklyPrice extends Model
public function weekObj()
{
return $this->belongsTo(Keywords::class, 'week');
return $this->belongsTo(Keywords::class, 'week', 'key');
}
public function createdBy()

View File

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class WormPhoto extends Model
{
use HasFactory;
protected $casts = [
'uploaded_at' => 'datetime',
];
protected $fillable = [
'device_id', 'url', 'uploaded_at',
];
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class WormReport extends Model
{
use HasFactory;
protected $casts = [
'data' => 'json',
'reported_at' => 'date',
];
protected $fillable = [
'device_id', 'agricultural_base_id', 'worm_num', 'data', 'reported_at',
];
}

View File

@ -6,6 +6,7 @@ use App\Services\LinkosService;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Relations\Relation;
use App\Iot\Linkos\HttpClient as LinkosHttpClient;
class AppServiceProvider extends ServiceProvider
{
@ -22,6 +23,8 @@ class AppServiceProvider extends ServiceProvider
return new LinkosService($config['key'] ?? '', $config['secret'] ?? '');
});
$this->registerLinkos();
}
/**
@ -48,4 +51,14 @@ class AppServiceProvider extends ServiceProvider
// ]);
}
protected function registerLinkos(): void
{
$this->app->singleton(LinkosHttpClient::class, function ($app) {
return new LinkosHttpClient(
(string) $app['config']->get('services.linkos.key'),
(string) $app['config']->get('services.linkos.secret')
);
});
}
}

View File

@ -0,0 +1,604 @@
<?php
namespace App\Services;
use App\Enums\DeviceType;
use App\Exceptions\BizException;
use App\Iot\BiAng\HttpClient;
use App\Models\Device;
use App\Models\DeviceLog;
use App\Models\InsecticidalLampDailyReport;
use App\Models\InsecticidalLampReport;
use App\Models\MeteorologicalMonitoringDailyLog;
use App\Models\MeteorologicalMonitoringLog;
use App\Models\SoilMonitoringDailyLog;
use App\Models\SoilMonitoringLog;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class BiAngDeviceService
{
/**
* 虫情设备/昆虫性诱设备 - 查询某个时间段内的图片
*/
public function getWormPhotos(Device $device, Carbon $start, Carbon $end): array
{
try {
$httpClient = $this->buildHttpClient($device);
} catch (BizException $e) {
return [];
}
return $httpClient->getWormPhotos($device->sn, $start, $end);
}
/**
* 虫情设备 - (识别款)图片虫数识别统计
*/
public function getWormStatistics(Device $device, Carbon $start, Carbon $end): array
{
try {
$httpClient = $this->buildHttpClient($device);
} catch (BizException $e) {
return [];
}
return $httpClient->getWormStatistics($device->sn, $start, $end);
}
/**
* 创建设备报告
*/
public function createReport(Device $device, Carbon $time): void
{
switch ($device->type) {
case DeviceType::Soil:
$this->createSoilReport($device, $time);
break;
case DeviceType::Meteorological:
$this->createMeteorologicalReport($device, $time);
break;
case DeviceType::InsecticidalLamp:
$this->createInsecticidalLampReport($device, $time);
break;
}
}
/**
* 创建土壤设备报告
*/
protected function createSoilReport(Device $device, Carbon $time): void
{
$reportedAt = $time->copy()->startOfHour();
/** @var \Illuminate\Database\Eloquent\Collection */
$logs = DeviceLog::where('device_id', $device->id)
->whereBetween('reported_at', [$reportedAt, $reportedAt->copy()->endOfHour()])
->oldest('reported_at')
->get();
if ($logs->isEmpty()) {
return;
}
$attributes = $logs->reduce(function (array $attributes, DeviceLog $log) {
if (is_array($data = $log->data)) {
foreach ($data as $k => $v) {
$attribute = match ($k) {
'soilAlkalineHydrolyzedNitrogen' => 'n',
'soilAvailablePotassium' => 'k',
'soilAvailablePhosphorus' => 'p',
'soilConductivity' => 'conductivity',
'soilTemperature' => 'temperature',
'soilMoisture' => 'humidity',
default => null,
};
if ($attribute) {
$attributes[$attribute] = $v;
}
}
}
return $attributes;
}, []);
$soilReport = SoilMonitoringLog::where([
'device_id' => $device->id,
'monitored_at' => $reportedAt,
])->first();
if ($soilReport === null) {
$lastSoilReport = SoilMonitoringLog::where([
'device_id' => $device->id,
'monitored_at' => $reportedAt->copy()->subHour(),
])->first();
$soilReport = $lastSoilReport?->replicate() ?: new SoilMonitoringLog();
$soilReport->fill([
'device_id' => $device->id,
'monitored_at' => $reportedAt,
'agricultural_base_id' => $device->agricultural_base_id,
]);
}
$soilReport->fill($attributes)->save();
}
/**
* 创建气象设备报告
*/
protected function createMeteorologicalReport(Device $device, Carbon $time): void
{
$reportedAt = $time->copy()->startOfHour();
/** @var \Illuminate\Database\Eloquent\Collection */
$logs = DeviceLog::where('device_id', $device->id)
->whereBetween('reported_at', [$reportedAt, $reportedAt->copy()->endOfHour()])
->oldest('reported_at')
->get();
if ($logs->isEmpty()) {
return;
}
$attributes = $logs->reduce(function (array $attributes, DeviceLog $log) {
if (is_array($data = $log->data)) {
foreach ($data as $k => $v) {
$attribute = match ($k) {
'rainfall' => 'moment_rainfall', //瞬时降雨量
'lightIntensity' => 'illumination',
'airTemperature' => 'air_temperature',
'airHumidity' => 'air_humidity',
'windDirection' => 'wind_degree',
'windSpeed' => 'wind_speed',
default => null,
};
if ($attribute) {
switch ($attribute) {
case 'moment_rainfall':
$attributes[$attribute] = bcadd($attributes[$attribute] ?? '0.00', $v, 2);
break;
case 'wind_degree':
$attributes['wind_direction'] = value(function ($v) {
if ($v >= 22.5 && $v < 67.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_NORTHEAST;
} elseif ($v >= 67.5 && $v < 112.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_EAST;
} elseif ($v >= 112.5 && $v < 157.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_SOUTHEAST;
} elseif ($v >= 157.5 && $v < 202.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_SOUTH;
} elseif ($v >= 202.5 && $v < 247.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_SOUTHWEST;
} elseif ($v >= 247.5 && $v < 292.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_WEST;
} elseif ($v >= 292.5 && $v < 337.5) {
return MeteorologicalMonitoringLog::WIND_DIRECTION_NORTHWEST;
}
return MeteorologicalMonitoringLog::WIND_DIRECTION_NORTH;
}, $v);
default:
$attributes[$attribute] = $v;
break;
}
}
}
}
return $attributes;
}, []);
# 累计雨量求和
$attributes['current_rainfall'] = DeviceLog::select(DB::raw("SUM((data->>'rainfall')::NUMERIC) as aggregate"))->where('device_id', $device->id)
->whereBetween('reported_at', [$reportedAt->copy()->startOfDay(), $reportedAt->copy()->endOfHour()])
->value('aggregate') ?: 0;
$meteorologicalReport = MeteorologicalMonitoringLog::where([
'device_id' => $device->id,
'monitored_at' => $reportedAt,
])->first();
if ($meteorologicalReport === null) {
$lastMeteorologicalReport = MeteorologicalMonitoringLog::where([
'device_id' => $device->id,
'monitored_at' => $reportedAt->copy()->subHour(),
])->first();
$meteorologicalReport = $lastMeteorologicalReport?->replicate() ?: new MeteorologicalMonitoringLog();
$meteorologicalReport->fill([
'device_id' => $device->id,
'monitored_at' => $reportedAt,
'agricultural_base_id' => $device->agricultural_base_id,
]);
}
$meteorologicalReport->fill($attributes)->save();
}
/**
* 创建杀虫灯设备报告
*/
protected function createInsecticidalLampReport(Device $device, Carbon $time): void
{
$reportedAt = $time->copy()->startOfHour();
/** @var \Illuminate\Database\Eloquent\Collection */
$logs = DeviceLog::where('device_id', $device->id)
->whereBetween('reported_at', [$reportedAt, $reportedAt->copy()->endOfHour()])
->oldest('reported_at')
->get();
if ($logs->isEmpty()) {
return;
}
$attributes = value(function ($logs) {
$data = [
'vol' => ['sum' => 0, 'count' => 0],
'sunVol' => ['sum' => 0, 'count' => 0],
'dct' => ['sum' => 0, 'count' => 0],
'temp' => ['sum' => 0, 'count' => 0],
'humidity' => ['sum' => 0, 'count' => 0],
'highVol' => ['sum' => 0, 'count' => 0],
];
/** @var \App\Models\DeviceLog */
foreach ($logs as $log) {
if (! is_array($log->data)) {
continue;
}
foreach ($data as $k => $item) {
$v = $log->data[$k] ?? null;
if (is_null($v)) {
continue;
}
$item['sum'] = bcadd($item['sum'], $v, 2);
$item['count']++;
$data[$k] = $item;
}
}
$attributes = [];
foreach ($data as $key => $item) {
$attribute = match ($key) {
'vol' => 'battery_vol',
'sunVol' => 'charging_vol',
'dct' => 'killed_num',
'temp' => 'air_temperature',
'humidity' => 'air_humidity',
'highVol' => 'high_vol',
};
if ($item['count'] > 0) {
if ($attribute === 'killed_num') {
$attributes[$attribute] = (int) $item['sum'];
} else {
$attributes[$attribute] = round(bcdiv($item['sum'], $item['count'], 2), 2);
}
} else {
$attributes[$attribute] = null;
}
}
return $attributes;
}, $logs);
/** @var \App\Models\InsecticidalLampReport */
$insecticidalLampReport = InsecticidalLampReport::firstOrNew([
'device_id' => $device->id,
'reported_at' => $reportedAt,
], [
'agricultural_base_id' => $device->agricultural_base_id,
]);
$insecticidalLampReport->fill($attributes)->save();
}
/**
* 创建设备每日报告
*/
public function createDailyReport(Device $device, Carbon $time): void
{
switch ($device->type) {
case DeviceType::Meteorological:
$this->createMeteorologicalDailyReport($device, $time);
break;
case DeviceType::Soil:
$this->createSoilDailyReport($device, $time);
break;
case DeviceType::InsecticidalLamp:
$this->createInsecticidalLampDailyReport($device, $time);
break;
}
}
/**
* 创建土壤设备每日报告
*/
protected function createSoilDailyReport(Device $device, Carbon $date): void
{
/** @var \Illuminate\Database\Eloquent\Collection */
$soilReports = SoilMonitoringLog::where('device_id', $device->id)
->whereDate('monitored_at', $date)
->oldest('monitored_at')
->get();
if ($soilReports->isEmpty()) {
return;
}
$attributes = value(function ($soilReports) {
$data = [
'n' => ['sum' => 0, 'count' => 0],
'p' => ['sum' => 0, 'count' => 0],
'k' => ['sum' => 0, 'count' => 0],
'conductivity' => ['sum' => 0, 'count' => 0],
'temperature' => ['sum' => 0, 'count' => 0],
'humidity' => ['sum' => 0, 'count' => 0],
'moisture' => ['sum' => 0, 'count' => 0],
];
foreach ($soilReports as $soilReport) {
foreach ($data as $k => $item) {
if (is_null($v = $soilReport->{$k})) {
continue;
}
$item['sum'] = bcadd($item['sum'], $v, 2);
$item['count']++;
$data[$k] = $item;
}
}
$attributes = [];
foreach ($data as $key => $item) {
$attributes[$key] = $item['count'] > 0 ? round(bcdiv($item['sum'], $item['count'], 2), 2) : null;
}
return $attributes;
}, $soilReports);
/** @var \App\Models\SoilDailyReport */
$soilDailyReport = SoilMonitoringDailyLog::firstOrNew([
'device_id' => $device->id,
'monitored_at' => $date->format('Y-m-d'),
], [
'agricultural_base_id' => $device->agricultural_base_id,
]);
$soilDailyReport->fill($attributes)->save();
}
/**
* 创建气象设备每日报告
*/
protected function createMeteorologicalDailyReport(Device $device, Carbon $date): void
{
/** @var \Illuminate\Database\Eloquent\Collection */
$meteorologicalReports = MeteorologicalMonitoringLog::where('device_id', $device->id)
->whereDate('monitored_at', $date)
->oldest('monitored_at')
->get();
if ($meteorologicalReports->isEmpty()) {
return;
}
$attributes = value(function ($meteorologicalReports) {
$data = [
'current_rainfall' => 0,
'illumination' => ['sum' => 0, 'count' => 0],
'air_temperature' => ['sum' => 0, 'count' => 0],
'air_humidity' => ['sum' => 0, 'count' => 0],
'wind_speed' => ['sum' => 0, 'count' => 0],
'wind_samples' => [],
];
foreach ($meteorologicalReports as $meteorologicalReport) {
foreach ($data as $k => $item) {
if ($k === 'wind_samples') {
if (is_null($meteorologicalReport->wind_degree) || is_null($meteorologicalReport->wind_speed)) {
continue;
}
$item[] = [
'wind_degree' => $meteorologicalReport->wind_degree, // 风向度数
'wind_speed' => $meteorologicalReport->wind_speed, // 风速
];
} elseif (! is_null($v = $meteorologicalReport->{$k})) {
if ($k === 'current_rainfall') {
$item = $v;
} else {
$item['sum'] = bcadd($item['sum'], $v, 2);
$item['count']++;
}
}
$data[$k] = $item;
}
}
$attributes = [];
foreach ($data as $key => $item) {
switch ($key) {
case 'current_rainfall':
$attributes['daily_rainfall'] = $item;
break;
case 'wind_samples':
if (! empty($item)) {
$attributes['wind_degree'] = value(function (array $windSamples) {
if (empty($windSamples)) {
return null;
}
$x = 0;
$y = 0;
foreach ($windSamples as $sample) {
if ($sample['wind_degree'] == 0 && $sample['wind_speed'] == 0) {
continue;
}
// 角度转弧度
$radian = deg2rad($sample['wind_degree']);
// $x += $sample['wind_speed'] * sin($radian);
// $y += $sample['wind_speed'] * cos($radian);
$x += sin($radian);
$y += cos($radian);
}
$degree = round(rad2deg(atan2($y, $x)));
if (($x > 0 || $x < 0) && $y < 0) {
$degree += 180;
} elseif ($x < 0 && $y > 0) {
$degree += 360;
}
return $degree;
}, $item);
$attributes['wind_direction'] = value(function ($windDegree) {
if (is_null($windDegree)) {
return null;
}
if ($windDegree >= 22.5 && $windDegree < 67.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_NORTHEAST;
} elseif ($windDegree >= 67.5 && $windDegree < 112.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_EAST;
} elseif ($windDegree >= 112.5 && $windDegree < 157.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_SOUTHEAST;
} elseif ($windDegree >= 157.5 && $windDegree < 202.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_SOUTH;
} elseif ($windDegree >= 202.5 && $windDegree < 247.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_SOUTHWEST;
} elseif ($windDegree >= 247.5 && $windDegree < 292.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_WEST;
} elseif ($windDegree >= 292.5 && $windDegree < 337.5) {
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_NORTHWEST;
}
return MeteorologicalMonitoringDailyLog::WIND_DIRECTION_NORTH;
}, $attributes['wind_degree']);
}
break;
default:
$attributes[$key] = $item['count'] > 0 ? round(bcdiv($item['sum'], $item['count'], 2), 2) : null;
break;
}
}
return $attributes;
}, $meteorologicalReports);
/** @var \App\Models\MeteorologicalMonitoringDailyLog */
$meteorologicalDailyReport = MeteorologicalMonitoringDailyLog::firstOrNew([
'device_id' => $device->id,
'monitored_at' => $date,
], [
'agricultural_base_id' => $device->agricultural_base_id,
]);
$meteorologicalDailyReport->fill($attributes)->save();
}
/**
* 杀虫灯每日报告
*/
protected function createInsecticidalLampDailyReport(Device $device, Carbon $date): void
{
/** @var \Illuminate\Database\Eloquent\Collection */
$insecticidalLampReports = InsecticidalLampReport::where('device_id', $device->id)
->whereDate('reported_at', $date)
->oldest('reported_at')
->get();
if ($insecticidalLampReports->isEmpty()) {
return;
}
$attributes = value(function ($insecticidalLampReports) {
$data = [
'killed_num' => ['sum' => 0, 'count' => 0],
'battery_vol' => ['sum' => 0, 'count' => 0],
'air_temperature' => ['sum' => 0, 'count' => 0],
'air_humidity' => ['sum' => 0, 'count' => 0],
'charging_vol' => ['sum' => 0, 'count' => 0],
'high_vol' => ['sum' => 0, 'count' => 0],
];
foreach ($insecticidalLampReports as $insecticidalLampReport) {
foreach ($data as $k => $item) {
if (is_null($v = $insecticidalLampReport->{$k})) {
continue;
}
$item['sum'] = bcadd($item['sum'], $v, 2);
$item['count']++;
$data[$k] = $item;
}
}
$attributes = [];
foreach ($data as $key => $item) {
if ($item['count'] > 0) {
if ($key === 'killed_num') {
$attributes[$key] = (int) $item['sum'];
} else {
$attributes[$key] = round(bcdiv($item['sum'], $item['count'], 2), 2);
}
} else {
$attributes[$key] = null;
}
}
return $attributes;
}, $insecticidalLampReports);
/** @var \App\Models\InsecticidalLampDailyReport */
$insecticidalLampDailyReport = InsecticidalLampDailyReport::firstOrNew([
'device_id' => $device->id,
'reported_at' => $date->format('Y-m-d'),
], [
'agricultural_base_id' => $device->agricultural_base_id,
]);
$insecticidalLampDailyReport->fill($attributes)->save();
}
/**
* 创建 HTTP 客户端
*/
public function buildHttpClient(Device $device): HttpClient
{
$config = json_decode($device->project?->value, true);
if (! is_array($config)) {
throw new BizException('账户信息未找到');
}
return new HttpClient($config['username'] ?? '', $config['password'] ?? '');
}
}

View File

@ -2,6 +2,7 @@
namespace App\Services;
use App\Enums\DeviceStatus;
use App\Enums\DeviceType;
use App\Enums\WindDirection;
use App\Exceptions\BizException;
@ -89,6 +90,8 @@ class LinkosDeviceLogService
throw new BizException("设备未找到, 设备编号: {$deviceId}");
}
Device::where('sn', $deviceId)->update(['status' => DeviceStatus::Online]);
$log = LinkosDeviceLog::create([
'device_id' => $deviceId,
'device_unit' => $deviceUnit,
@ -235,7 +238,7 @@ class LinkosDeviceLogService
}
$log->save();
}
/**

View File

@ -27,7 +27,7 @@ return new class extends Migration
$table->timestamps();
$table->index('agricultural_base_id');
$table->unique(['device_id', 'monitored_at']);
$table->unique(['device_id', 'monitored_at'], 'uq_device_id_monitored_at');
});
}

View File

@ -27,7 +27,7 @@ return new class extends Migration
$table->timestamps();
$table->index('agricultural_base_id');
$table->unique(['device_id', 'monitored_at']);
$table->unique(['device_id', 'monitored_at'], 'uq_device_id_monitored_at');
});
}

View File

@ -27,7 +27,7 @@ return new class extends Migration
$table->timestamps();
$table->index('agricultural_base_id');
$table->unique(['device_id', 'monitored_at']);
$table->unique(['device_id', 'monitored_at'], 'uq_device_id_monitored_at');
});
}

View File

@ -33,7 +33,7 @@ return new class extends Migration
$table->timestamps();
$table->index('agricultural_base_id');
$table->unique(['device_id', 'monitored_at']);
$table->unique(['device_id', 'monitored_at'], 'uq_device_id_monitored_at');
});
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('worm_reports', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('device_id');
$table->unsignedBigInteger('agricultural_base_id')->comment('农业基地ID');
$table->unsignedInteger('worm_num')->comment('虫子数量');
$table->json('data')->nullable();
$table->date('reported_at')->comment('报告日期');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('worm_reports');
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('devices', function (Blueprint $table) {
$table->string('supplier_key')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('devices', function (Blueprint $table) {
$table->dropColumn(['supplier_key']);
});
}
};

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('device_logs', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('device_id');
$table->json('data')->nullable();
$table->timestamp('reported_at');
$table->timestamps();
$table->index(['device_id', 'reported_at']);
$table->index('reported_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('device_logs');
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('devices', function (Blueprint $table) {
$table->string('project_key')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('devices', function (Blueprint $table) {
$table->dropColumn(['project_key']);
});
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('meteorological_monitoring_daily_logs', function (Blueprint $table) {
$table->decimal('wind_degree', 5, 2)->nullable()->comment('风向度数')->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('meteorological_monitoring_daily_logs', function (Blueprint $table) {
$table->integer('wind_degree')->nullable()->comment('风向度数')->change();
});
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('meteorological_monitoring_logs', function (Blueprint $table) {
$table->decimal('wind_degree', 5, 2)->nullable()->comment('风向度数')->change();
$table->decimal('illumination', 10, 2)->nullable()->comment('光照度 (单位: Lux)')->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('meteorological_monitoring_logs', function (Blueprint $table) {
$table->integer('wind_degree')->nullable()->comment('风向度数')->change();
$table->integer('illumination')->nullable()->comment('光照度 (单位: Lux)')->change();
});
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('soil_monitoring_logs', function (Blueprint $table) {
$table->decimal('n', 8, 2)->nullable()->comment('氮 (单位: mg/kg)')->change();
$table->decimal('p', 8, 2)->nullable()->comment('磷 (单位: mg/kg)')->change();
$table->decimal('k', 8, 2)->nullable()->comment('钾 (单位: mg/kg)')->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('soil_monitoring_logs', function (Blueprint $table) {
$table->integer('n')->nullable()->comment('氮 (单位: mg/kg)')->change();
$table->integer('p')->nullable()->comment('磷 (单位: mg/kg)')->change();
$table->integer('k')->nullable()->comment('钾 (单位: mg/kg)')->change();
});
}
};

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('insecticidal_lamp_reports', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('device_id');
$table->unsignedBigInteger('agricultural_base_id')->comment('农业基地ID');
$table->double('battery_vol')->nullable()->comment('蓄电池电压');
$table->unsignedInteger('killed_num')->default(0)->comment('杀虫数');
$table->double('air_temperature')->nullable()->comment('大气气温');
$table->double('air_humidity')->nullable()->comment('大气湿度');
$table->double('charging_vol')->nullable()->comment('充电电压');
$table->double('high_vol')->nullable()->comment('高压值');
$table->timestamp('reported_at')->comment('监控日期');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('insecticidal_lamp_reports');
}
};

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('insecticidal_lamp_daily_reports', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('device_id');
$table->unsignedBigInteger('agricultural_base_id')->comment('农业基地ID');
$table->double('battery_vol')->nullable()->comment('蓄电池电压');
$table->unsignedInteger('killed_num')->default(0)->comment('杀虫数');
$table->double('air_temperature')->nullable()->comment('大气气温');
$table->double('air_humidity')->nullable()->comment('大气湿度');
$table->double('charging_vol')->nullable()->comment('充电电压');
$table->double('high_vol')->nullable()->comment('高压值');
$table->date('reported_at')->comment('监控日期');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('insecticidal_lamp_reports');
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('worm_photos', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('device_id');
$table->text('url')->nullable();
$table->timestamp('uploaded_at')->comment('上传时间');
$table->timestamps();
$table->index('uploaded_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('worm_photos');
}
};

View File

@ -32,6 +32,62 @@ class KeywordsTableSeeder extends Seeder
['key' => 'crops-cate-lingye', 'name' => '林业', 'type_key' => 'crops-category', 'value' => ''],
['key' => 'crops-cate-activity', 'name' => '其他', 'type_key' => 'crops-category', 'value' => ''],
]],
[
'key' => 'industry_key',
'name' => '产业类型',
'value' => '',
'list' => [
['key' => 'industry_1', 'name' => '稻渔综合种养', 'value' => 1],
['key' => 'industry_2', 'name' => '优品柑桔种植', 'value' => 2],
['key' => 'industry_3', 'name' => '高梁产业', 'value' => 3],
['key' => 'industry_4', 'name' => '油茶产业', 'value' => 4],
['key' => 'industry_5', 'name' => '生猪产业分布点', 'value' => 5],
['key' => 'industry_6', 'name' => '肉羊产业分布点', 'value' => 6],
],
],
[
'key' => 'device-supplier',
'name' => '设备供应商',
'value' => '',
'list' => [
['key' => 'device-supplier-linkos', 'name' => '慧联无限', 'value' => ''],
['key' => 'device-supplier-biang', 'name' => '比昂', 'value' => ''],
['key' => 'device-supplier-yunfei', 'name' => '云飞', 'value' => ''],
['key' => 'device-supplier-other', 'name' => '其它', 'value' => ''],
],
],
[
'key' => 'device-project',
'name' => '设备项目',
'value' => '',
'list' => [
[
'key' => 'device-project-cyxdnyyq',
'name' => '成渝现代高效特色农业带合作园区',
'value' => json_encode([
'username' => '成渝现代高效特色农业带合作园区',
'password' => '888888',
]),
],
[
'key' => 'device-project-nyncj',
'name' => '隆昌市农业农村局',
'value' => json_encode([
'username' => '隆昌市农业农村局',
'password' => '888888',
]),
],
[
'key' => 'device-project-syqshc',
'name' => '隆昌市石燕桥镇三合村股份经济联合社',
'value' => json_encode([
'username' => '隆昌市石燕桥镇三合村股份经济联合社',
'password' => '888888',
]),
],
['key' => 'device-project-other', 'name' => '其它', 'value' => ''],
],
]
];
$list[] = value(function () {
@ -53,14 +109,15 @@ class KeywordsTableSeeder extends Seeder
private function createKeywords($keywords, $parentType = null)
{
foreach ($keywords as $item) {
foreach ($keywords as $i => $item) {
if ($parentType) {
$type = Keywords::create([
'name' => $item['name'],
'key' => $item['key'] ?? $parentType->key.($parentType + 1),
'key' => $item['key'] ?? $parentType->key.($i + 1),
'type_key' => $parentType->key,
'level' => ($parentType->level ?? 1) + 1,
'parent_id' => $parentType->id,
'value' => $item['value'],
]);
} else {
$type = Keywords::create(Arr::except($item, 'list'));

View File

@ -17,7 +17,9 @@ use Illuminate\Support\Facades\Route;
Route::post('auth/login', [AuthController::class, 'login']);
Route::group(['middleware' => 'auth:sanctum'], function () {
Route::group([
'middleware' => ['auth:sanctum'],
], function () {
Route::post('web/upload', [WebController::class, 'upload']);
@ -54,6 +56,8 @@ Route::group(['middleware' => 'auth:sanctum'], function () {
Route::apiResource('crop-flows', CropFlowController::class)->names('crops_flow');
//设备管理
Route::apiResource('devices', DeviceController::class)->names('device');
Route::get('devices/{device}/worm-statics', [DeviceController::class, 'wormStatics']);
Route::get('devices/{device}/worm-photos', [DeviceController::class, 'wormPhotos']);
Route::put('devices-update-recommend/{device}', [DeviceController::class, 'updateRecommendStatus']);
Route::get('devices-num', [DeviceController::class, 'typeStatusNum'])->name('device.type_status_num');
Route::get('monitoring-data', [DeviceController::class, 'timeZoneList']);

View File

@ -1,11 +1,15 @@
<?php
use App\Http\Controllers\Callback\LinkosController;
use Illuminate\Support\Facades\Route;
use App\Http\Middleware\ApiCustomToken;
use App\Http\Controllers\Callback\YunFeiController;
use App\Http\Controllers\ThirdApi\SendSmsController;
use App\Http\Middleware\ApiCustomToken;
use Illuminate\Support\Facades\Route;
Route::post('callback/linkos', LinkosController::class);
Route::post('callback/yunfei/insecticidal-lamp-notify', [YunFeiController::class, 'insecticidalLampNotify']);
Route::post('callback/yunfei/worm-notify', [YunFeiController::class, 'wormNotify']);
Route::post('callback/yunfei/worm-photo-notify', [YunFeiController::class, 'wormPhotoNotify']);
Route::group(['prefix'=>'third'], function(){
Route::middleware([ApiCustomToken::class])->group(function(){