refactor: remove syncing
This commit is contained in:
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Actions;
|
||||
|
||||
use App\Models\Terminal;
|
||||
use App\Services\TerminalSyncService;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\BulkAction;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
/**
|
||||
* 同步终端配置Action
|
||||
*/
|
||||
class SyncConfigAction
|
||||
{
|
||||
/**
|
||||
* 创建单个终端同步Action
|
||||
*
|
||||
* @return Action
|
||||
*/
|
||||
public static function make(): Action
|
||||
{
|
||||
return Action::make('sync')
|
||||
->label('同步配置')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('同步终端配置')
|
||||
->modalDescription('确定要将配置同步到此终端吗?')
|
||||
->modalSubmitActionLabel('确认同步')
|
||||
->action(function (Terminal $record) {
|
||||
$service = app(TerminalSyncService::class);
|
||||
$log = $service->syncConfiguration($record);
|
||||
|
||||
Notification::make()
|
||||
->title('同步任务已启动')
|
||||
->body("终端 {$record->name} 的配置同步任务已加入队列")
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建批量同步Action
|
||||
*
|
||||
* @return BulkAction
|
||||
*/
|
||||
public static function makeBulk(): BulkAction
|
||||
{
|
||||
return BulkAction::make('batchSync')
|
||||
->label('批量同步')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('批量同步终端配置')
|
||||
->modalDescription(fn ($records) => "确定要同步 {$records->count()} 个终端的配置吗?")
|
||||
->modalSubmitActionLabel('确认同步')
|
||||
->action(function ($records) {
|
||||
$service = app(TerminalSyncService::class);
|
||||
$terminalIds = $records->pluck('id')->toArray();
|
||||
$logs = $service->batchSync($terminalIds);
|
||||
|
||||
Notification::make()
|
||||
->title('批量同步任务已启动')
|
||||
->body("已为 " . count($logs) . " 个终端创建同步任务")
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
->deselectRecordsAfterCompletion();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\TerminalResource\Pages;
|
||||
use App\Filament\Actions\SyncConfigAction;
|
||||
use App\Models\Terminal;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
@@ -86,50 +85,34 @@ class TerminalResource extends Resource
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Forms\Components\Section::make('组态图配置')
|
||||
Forms\Components\Section::make('组态配置')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('diagram_url')
|
||||
->label('组态图URL')
|
||||
->label('组态界面地址')
|
||||
->url()
|
||||
->maxLength(500)
|
||||
->placeholder('https://example.com/diagram.png')
|
||||
->helperText('组态图的访问地址'),
|
||||
->helperText('组态界面的访问地址'),
|
||||
]),
|
||||
|
||||
Forms\Components\Section::make('SCADA网关配置')
|
||||
Forms\Components\Section::make('网关配置')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('scada_data_url')
|
||||
->label('SCADA数据查询URL')
|
||||
->label('数据查询URL')
|
||||
->url()
|
||||
->maxLength(500)
|
||||
->placeholder('http://gateway:8080/api/data')
|
||||
->helperText('OPC UA HTTP网关的数据查询地址'),
|
||||
->helperText('网关的数据查询地址'),
|
||||
|
||||
Forms\Components\TextInput::make('scada_tags_url')
|
||||
->label('SCADA点位定义URL')
|
||||
->label('点位定义URL')
|
||||
->url()
|
||||
->maxLength(500)
|
||||
->placeholder('http://gateway:8080/api/tags')
|
||||
->helperText('OPC UA HTTP网关的点位定义查询地址'),
|
||||
->helperText('网关的点位定义查询地址'),
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Forms\Components\Section::make('显示配置')
|
||||
->schema([
|
||||
Forms\Components\KeyValue::make('display_config')
|
||||
->label('显示参数')
|
||||
->keyLabel('参数名称')
|
||||
->valueLabel('参数值')
|
||||
->addActionLabel('添加参数')
|
||||
->helperText('配置终端的显示参数,如分辨率、刷新率等')
|
||||
->default([
|
||||
'resolution' => '1920x1080',
|
||||
'refresh_rate' => '60',
|
||||
'orientation' => 'landscape',
|
||||
'brightness' => '80',
|
||||
]),
|
||||
]),
|
||||
|
||||
Forms\Components\Section::make('知识库关联')
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('knowledgeBaseAssociations')
|
||||
@@ -158,7 +141,8 @@ class TerminalResource extends Resource
|
||||
->reorderableWithButtons()
|
||||
->addActionLabel('添加知识库')
|
||||
->reorderableWithDragAndDrop(false)
|
||||
->itemLabel(fn (array $state): ?string =>
|
||||
->itemLabel(
|
||||
fn(array $state): ?string =>
|
||||
\App\Models\KnowledgeBase::find($state['id'])?->name ?? '未选择'
|
||||
)
|
||||
->collapsed()
|
||||
@@ -195,7 +179,8 @@ class TerminalResource extends Resource
|
||||
->reorderableWithButtons()
|
||||
->addActionLabel('添加指引')
|
||||
->reorderableWithDragAndDrop(false)
|
||||
->itemLabel(fn (array $state): ?string =>
|
||||
->itemLabel(
|
||||
fn(array $state): ?string =>
|
||||
\App\Models\Guide::find($state['id'])?->name ?? '未选择'
|
||||
)
|
||||
->collapsed()
|
||||
@@ -216,16 +201,16 @@ class TerminalResource extends Resource
|
||||
->placeholderText('请输入AI提示词模板...')
|
||||
->disablePreview()
|
||||
->columnSpan(2),
|
||||
|
||||
|
||||
Forms\Components\Grid::make(1)
|
||||
->schema([
|
||||
Forms\Components\Placeholder::make('template_selector')
|
||||
->label('模板库')
|
||||
->content(fn () => view('filament.components.prompt-template-selector')),
|
||||
|
||||
->content(fn() => view('filament.components.prompt-template-selector')),
|
||||
|
||||
Forms\Components\Placeholder::make('variable_helper')
|
||||
->label('变量参考')
|
||||
->content(fn () => view('filament.components.prompt-variable-helper')),
|
||||
->content(fn() => view('filament.components.prompt-variable-helper')),
|
||||
])
|
||||
->columnSpan(1),
|
||||
]),
|
||||
@@ -296,33 +281,6 @@ class TerminalResource extends Resource
|
||||
->falseColor('danger')
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('latestSyncLog.status')
|
||||
->label('同步状态')
|
||||
->badge()
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'pending' => '待同步',
|
||||
'syncing' => '同步中',
|
||||
'synced' => '已同步',
|
||||
'failed' => '失败',
|
||||
default => '未知',
|
||||
})
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'pending' => 'warning',
|
||||
'syncing' => 'info',
|
||||
'synced' => 'success',
|
||||
'failed' => 'danger',
|
||||
default => 'gray',
|
||||
})
|
||||
->icon(fn (string $state): string => match ($state) {
|
||||
'pending' => 'heroicon-o-clock',
|
||||
'syncing' => 'heroicon-o-arrow-path',
|
||||
'synced' => 'heroicon-o-check-circle',
|
||||
'failed' => 'heroicon-o-x-circle',
|
||||
default => 'heroicon-o-question-mark-circle',
|
||||
})
|
||||
->placeholder('从未同步')
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('last_online_at')
|
||||
->label('最后在线时间')
|
||||
->dateTime('Y-m-d H:i:s')
|
||||
@@ -348,17 +306,8 @@ class TerminalResource extends Resource
|
||||
->placeholder('全部')
|
||||
->trueLabel('在线')
|
||||
->falseLabel('离线'),
|
||||
|
||||
Tables\Filters\Filter::make('station_id')
|
||||
->label('已绑定线站')
|
||||
->query(fn (Builder $query): Builder => $query->whereNotNull('station_id')),
|
||||
|
||||
Tables\Filters\Filter::make('has_diagram')
|
||||
->label('已配置组态图')
|
||||
->query(fn (Builder $query): Builder => $query->whereNotNull('diagram_url')),
|
||||
])
|
||||
->actions([
|
||||
SyncConfigAction::make(),
|
||||
Tables\Actions\ViewAction::make()
|
||||
->label('查看'),
|
||||
Tables\Actions\EditAction::make()
|
||||
@@ -368,7 +317,6 @@ class TerminalResource extends Resource
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
SyncConfigAction::makeBulk(),
|
||||
Tables\Actions\DeleteBulkAction::make()
|
||||
->label('批量删除'),
|
||||
]),
|
||||
@@ -380,7 +328,7 @@ class TerminalResource extends Resource
|
||||
->collapsible(),
|
||||
Tables\Grouping\Group::make('is_online')
|
||||
->label('按在线状态分组')
|
||||
->getTitleFromRecordUsing(fn (Terminal $record): string => $record->is_online ? '在线' : '离线')
|
||||
->getTitleFromRecordUsing(fn(Terminal $record): string => $record->is_online ? '在线' : '离线')
|
||||
->collapsible(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -43,24 +43,16 @@ class ViewTerminal extends ViewRecord
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Infolists\Components\Section::make('组态图配置')
|
||||
Infolists\Components\Section::make('组态配置')
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('diagram_url')
|
||||
->label('组态图URL')
|
||||
->label('组态界面地址')
|
||||
->copyable()
|
||||
->placeholder('未设置')
|
||||
->url(fn ($state) => $state)
|
||||
->url(fn($state) => $state)
|
||||
->openUrlInNewTab(),
|
||||
]),
|
||||
|
||||
Infolists\Components\Section::make('显示配置')
|
||||
->schema([
|
||||
Infolists\Components\KeyValueEntry::make('display_config')
|
||||
->label('显示参数')
|
||||
->keyLabel('参数名称')
|
||||
->valueLabel('参数值'),
|
||||
]),
|
||||
|
||||
Infolists\Components\Section::make('知识库关联')
|
||||
->schema([
|
||||
Infolists\Components\RepeatableEntry::make('knowledgeBases')
|
||||
@@ -103,49 +95,6 @@ class ViewTerminal extends ViewRecord
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Infolists\Components\Section::make('同步历史')
|
||||
->schema([
|
||||
Infolists\Components\RepeatableEntry::make('syncLogs')
|
||||
->label('同步记录')
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('status')
|
||||
->label('状态')
|
||||
->badge()
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'pending' => '待同步',
|
||||
'syncing' => '同步中',
|
||||
'synced' => '已同步',
|
||||
'failed' => '失败',
|
||||
default => '未知',
|
||||
})
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'pending' => 'warning',
|
||||
'syncing' => 'info',
|
||||
'synced' => 'success',
|
||||
'failed' => 'danger',
|
||||
default => 'gray',
|
||||
}),
|
||||
Infolists\Components\TextEntry::make('created_at')
|
||||
->label('创建时间')
|
||||
->dateTime('Y-m-d H:i:s'),
|
||||
Infolists\Components\TextEntry::make('synced_at')
|
||||
->label('同步完成时间')
|
||||
->dateTime('Y-m-d H:i:s')
|
||||
->placeholder('未完成'),
|
||||
Infolists\Components\TextEntry::make('error_message')
|
||||
->label('错误信息')
|
||||
->placeholder('无')
|
||||
->color('danger')
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns(3)
|
||||
->placeholder('暂无同步记录')
|
||||
->contained(false),
|
||||
])
|
||||
->description('显示最近的配置同步记录,按时间倒序排列')
|
||||
->collapsible()
|
||||
->collapsed(),
|
||||
|
||||
Infolists\Components\Section::make('时间信息')
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('created_at')
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Filament\Widgets;
|
||||
use App\Models\Terminal;
|
||||
use App\Models\TerminalKnowledgeBase;
|
||||
use App\Models\TerminalPrompt;
|
||||
use App\Models\TerminalSyncLog;
|
||||
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
|
||||
@@ -25,15 +24,6 @@ class TerminalStatsWidget extends BaseWidget
|
||||
// 统计提示词
|
||||
$totalPrompts = TerminalPrompt::count();
|
||||
|
||||
// 统计最近同步
|
||||
$recentSyncs = TerminalSyncLog::where('created_at', '>=', now()->subDay())
|
||||
->where('status', 'success')
|
||||
->count();
|
||||
|
||||
$failedSyncs = TerminalSyncLog::where('created_at', '>=', now()->subDay())
|
||||
->where('status', 'failed')
|
||||
->count();
|
||||
|
||||
return [
|
||||
Stat::make('终端总数', $totalTerminals)
|
||||
->description("{$onlineTerminals} 个在线")
|
||||
@@ -50,16 +40,6 @@ class TerminalStatsWidget extends BaseWidget
|
||||
->description('终端提示词总数')
|
||||
->descriptionIcon('heroicon-m-chat-bubble-left-right')
|
||||
->color('success'),
|
||||
|
||||
Stat::make('今日同步成功', $recentSyncs)
|
||||
->description('最近24小时')
|
||||
->descriptionIcon('heroicon-m-arrow-path')
|
||||
->color('success'),
|
||||
|
||||
Stat::make('今日同步失败', $failedSyncs)
|
||||
->description($failedSyncs > 0 ? '需要检查' : '运行正常')
|
||||
->descriptionIcon($failedSyncs > 0 ? 'heroicon-m-exclamation-triangle' : 'heroicon-m-check-circle')
|
||||
->color($failedSyncs > 0 ? 'danger' : 'success'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ class TerminalApiController extends Controller
|
||||
'diagram_url' => $terminal->diagram_url,
|
||||
'scada_data_url' => $terminal->scada_data_url,
|
||||
'scada_tags_url' => $terminal->scada_tags_url,
|
||||
'display_config' => $terminal->display_config,
|
||||
],
|
||||
'system_prompt' => $systemPrompt,
|
||||
'guide_count' => $guideCount,
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Terminal;
|
||||
use App\Models\TerminalSyncLog;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 终端配置同步任务
|
||||
*/
|
||||
class SyncTerminalConfigJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* 任务最大尝试次数
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $tries = 3;
|
||||
|
||||
/**
|
||||
* 任务超时时间(秒)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $timeout = 60;
|
||||
|
||||
/**
|
||||
* 创建新的任务实例
|
||||
*
|
||||
* @param Terminal $terminal
|
||||
* @param TerminalSyncLog $log
|
||||
*/
|
||||
public function __construct(
|
||||
public Terminal $terminal,
|
||||
public TerminalSyncLog $log
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 执行任务
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
// 更新状态为同步中
|
||||
$this->log->update(['status' => 'syncing']);
|
||||
|
||||
Log::info('开始同步终端配置', [
|
||||
'terminal_id' => $this->terminal->id,
|
||||
'terminal_name' => $this->terminal->name,
|
||||
'log_id' => $this->log->id,
|
||||
]);
|
||||
|
||||
// 模拟同步过程(因为没有真实的终端API)
|
||||
// 在实际环境中,这里应该调用真实的终端API
|
||||
$this->simulateSync();
|
||||
|
||||
// 更新状态为已同步
|
||||
$this->log->update([
|
||||
'status' => 'synced',
|
||||
'synced_at' => now(),
|
||||
'error_message' => null,
|
||||
]);
|
||||
|
||||
Log::info('终端配置同步成功', [
|
||||
'terminal_id' => $this->terminal->id,
|
||||
'log_id' => $this->log->id,
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// 记录错误日志
|
||||
Log::error('终端配置同步失败', [
|
||||
'terminal_id' => $this->terminal->id,
|
||||
'log_id' => $this->log->id,
|
||||
'error' => $e->getMessage(),
|
||||
'attempt' => $this->attempts(),
|
||||
]);
|
||||
|
||||
// 更新状态为失败
|
||||
$this->log->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
// 如果还有重试次数,则重新抛出异常以触发重试
|
||||
if ($this->attempts() < $this->tries) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟同步过程
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function simulateSync(): void
|
||||
{
|
||||
// 模拟网络延迟
|
||||
sleep(2);
|
||||
|
||||
// 随机模拟成功或失败(90%成功率)
|
||||
if (rand(1, 100) <= 10) {
|
||||
throw new \Exception('模拟同步失败:网络连接超时');
|
||||
}
|
||||
|
||||
// 在实际环境中,这里应该是类似这样的代码:
|
||||
// $response = Http::timeout(30)
|
||||
// ->post($this->terminal->sync_url, [
|
||||
// 'config' => $this->log->config_snapshot,
|
||||
// ]);
|
||||
//
|
||||
// if (!$response->successful()) {
|
||||
// throw new \Exception('同步失败:' . $response->body());
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务失败时的处理
|
||||
*
|
||||
* @param \Throwable $exception
|
||||
* @return void
|
||||
*/
|
||||
public function failed(\Throwable $exception): void
|
||||
{
|
||||
Log::error('终端配置同步任务最终失败', [
|
||||
'terminal_id' => $this->terminal->id,
|
||||
'log_id' => $this->log->id,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
// 确保状态更新为失败
|
||||
$this->log->update([
|
||||
'status' => 'failed',
|
||||
'error_message' => '同步失败(已达最大重试次数):' . $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算重试延迟时间(秒)
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function backoff(): int
|
||||
{
|
||||
// 指数退避:第1次重试等待10秒,第2次等待30秒
|
||||
return [10, 30][$this->attempts() - 1] ?? 60;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ class Terminal extends Model
|
||||
'diagram_url',
|
||||
'scada_data_url',
|
||||
'scada_tags_url',
|
||||
'display_config',
|
||||
'is_online',
|
||||
'last_online_at',
|
||||
];
|
||||
@@ -39,7 +38,6 @@ class Terminal extends Model
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'display_config' => 'array',
|
||||
'is_online' => 'boolean',
|
||||
'last_online_at' => 'datetime',
|
||||
];
|
||||
@@ -81,26 +79,6 @@ class Terminal extends Model
|
||||
return $this->hasOne(TerminalPrompt::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取终端的同步日志
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function syncLogs()
|
||||
{
|
||||
return $this->hasMany(TerminalSyncLog::class)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取终端的最新同步日志
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function latestSyncLog()
|
||||
{
|
||||
return $this->hasOne(TerminalSyncLog::class)->latestOfMany();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置活动日志选项
|
||||
*
|
||||
@@ -109,7 +87,7 @@ class Terminal extends Model
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['name', 'code', 'station_id', 'diagram_url', 'display_config'])
|
||||
->logOnly(['name', 'code', 'station_id', 'diagram_url'])
|
||||
->logOnlyDirty()
|
||||
->setDescriptionForEvent(fn(string $eventName) => "终端已{$eventName}");
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TerminalSyncLog extends Model
|
||||
{
|
||||
/**
|
||||
* 可批量赋值的属性
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'terminal_id',
|
||||
'status',
|
||||
'config_snapshot',
|
||||
'synced_at',
|
||||
'error_message',
|
||||
];
|
||||
|
||||
/**
|
||||
* 属性类型转换
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'config_snapshot' => 'array',
|
||||
'synced_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取同步日志所属的终端
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function terminal()
|
||||
{
|
||||
return $this->belongsTo(Terminal::class);
|
||||
}
|
||||
}
|
||||
@@ -65,18 +65,6 @@ class TerminalPolicy
|
||||
return $user->can('terminal.delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否可以同步终端配置
|
||||
*
|
||||
* @param User $user
|
||||
* @param Terminal $terminal
|
||||
* @return bool
|
||||
*/
|
||||
public function sync(User $user, Terminal $terminal): bool
|
||||
{
|
||||
return $user->can('terminal.sync');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否可以恢复已删除的终端
|
||||
*
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Terminal;
|
||||
use App\Models\TerminalSyncLog;
|
||||
use App\Jobs\SyncTerminalConfigJob;
|
||||
|
||||
/**
|
||||
* 终端配置同步服务
|
||||
*/
|
||||
class TerminalSyncService
|
||||
{
|
||||
/**
|
||||
* 同步终端配置
|
||||
*
|
||||
* @param Terminal $terminal
|
||||
* @return TerminalSyncLog
|
||||
*/
|
||||
public function syncConfiguration(Terminal $terminal): TerminalSyncLog
|
||||
{
|
||||
// 创建同步日志记录
|
||||
$log = TerminalSyncLog::create([
|
||||
'terminal_id' => $terminal->id,
|
||||
'status' => 'pending',
|
||||
'config_snapshot' => $this->getConfigSnapshot($terminal),
|
||||
]);
|
||||
|
||||
// 触发异步同步任务
|
||||
dispatch(new SyncTerminalConfigJob($terminal, $log));
|
||||
|
||||
return $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取终端配置快照
|
||||
*
|
||||
* @param Terminal $terminal
|
||||
* @return array
|
||||
*/
|
||||
public function getConfigSnapshot(Terminal $terminal): array
|
||||
{
|
||||
// 加载关联数据
|
||||
$terminal->load(['knowledgeBases', 'prompt']);
|
||||
|
||||
return [
|
||||
'terminal' => [
|
||||
'id' => $terminal->id,
|
||||
'name' => $terminal->name,
|
||||
'code' => $terminal->code,
|
||||
'ip_address' => $terminal->ip_address,
|
||||
'station_id' => $terminal->station_id,
|
||||
'diagram_url' => $terminal->diagram_url,
|
||||
'display_config' => $terminal->display_config,
|
||||
],
|
||||
'knowledge_bases' => $terminal->knowledgeBases->map(function ($kb) {
|
||||
return [
|
||||
'id' => $kb->id,
|
||||
'name' => $kb->name,
|
||||
'priority' => $kb->pivot->priority,
|
||||
];
|
||||
})->toArray(),
|
||||
'prompt' => $terminal->prompt ? [
|
||||
'prompt_template' => $terminal->prompt->prompt_template,
|
||||
'variables' => $terminal->prompt->variables,
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新同步状态
|
||||
*
|
||||
* @param TerminalSyncLog $log
|
||||
* @param string $status
|
||||
* @param string|null $errorMessage
|
||||
* @return void
|
||||
*/
|
||||
public function updateSyncStatus(TerminalSyncLog $log, string $status, ?string $errorMessage = null): void
|
||||
{
|
||||
$data = ['status' => $status];
|
||||
|
||||
if ($status === 'synced') {
|
||||
$data['synced_at'] = now();
|
||||
}
|
||||
|
||||
if ($errorMessage) {
|
||||
$data['error_message'] = $errorMessage;
|
||||
}
|
||||
|
||||
$log->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步终端配置
|
||||
*
|
||||
* @param array $terminalIds
|
||||
* @return array
|
||||
*/
|
||||
public function batchSync(array $terminalIds): array
|
||||
{
|
||||
$logs = [];
|
||||
|
||||
foreach ($terminalIds as $terminalId) {
|
||||
$terminal = Terminal::find($terminalId);
|
||||
if ($terminal) {
|
||||
$logs[] = $this->syncConfiguration($terminal);
|
||||
}
|
||||
}
|
||||
|
||||
return $logs;
|
||||
}
|
||||
}
|
||||
@@ -30,12 +30,6 @@ class TerminalFactory extends Factory
|
||||
'ip_address' => fake()->localIpv4(),
|
||||
'station_id' => null, // 需要关联实际的线站ID
|
||||
'diagram_url' => fake()->imageUrl(1920, 1080, 'diagram', true),
|
||||
'display_config' => [
|
||||
'resolution' => fake()->randomElement(['1920x1080', '2560x1440', '3840x2160']),
|
||||
'refresh_rate' => fake()->randomElement([30, 60, 120]),
|
||||
'orientation' => fake()->randomElement(['landscape', 'portrait']),
|
||||
'brightness' => fake()->numberBetween(50, 100),
|
||||
],
|
||||
'is_online' => fake()->boolean(70), // 70%概率在线
|
||||
'last_online_at' => fake()->dateTimeBetween('-7 days', 'now'),
|
||||
];
|
||||
@@ -63,21 +57,6 @@ class TerminalFactory extends Factory
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定终端为高分辨率配置
|
||||
*/
|
||||
public function highResolution(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'display_config' => [
|
||||
'resolution' => '3840x2160',
|
||||
'refresh_rate' => 60,
|
||||
'orientation' => 'landscape',
|
||||
'brightness' => 80,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定终端为生产线终端
|
||||
*/
|
||||
|
||||
@@ -16,9 +16,11 @@ return new class extends Migration
|
||||
$table->string('name')->comment('终端名称');
|
||||
$table->string('code', 100)->unique()->comment('终端编码');
|
||||
$table->string('ip_address', 45)->nullable()->comment('IP地址');
|
||||
$table->string('mac_address', 17)->nullable()->unique()->comment('MAC地址 (AA:BB:CC:DD:EE:FF)');
|
||||
$table->string('station_id', 50)->nullable()->comment('线站ID');
|
||||
$table->string('diagram_url', 500)->nullable()->comment('组态图URL');
|
||||
$table->json('display_config')->nullable()->comment('显示配置');
|
||||
$table->string('diagram_url', 500)->nullable()->comment('组态界面地址');
|
||||
$table->string('scada_data_url', 500)->nullable()->comment('网关数据查询地址');
|
||||
$table->string('scada_tags_url', 500)->nullable()->comment('网关点位定义查询地址');
|
||||
$table->boolean('is_online')->default(false)->comment('在线状态');
|
||||
$table->timestamp('last_online_at')->nullable()->comment('最后在线时间');
|
||||
$table->timestamps();
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('terminal_sync_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('terminal_id')->comment('终端ID');
|
||||
$table->enum('status', ['pending', 'syncing', 'synced', 'failed'])
|
||||
->default('pending')
|
||||
->comment('同步状态');
|
||||
$table->json('config_snapshot')->nullable()->comment('配置快照');
|
||||
$table->timestamp('synced_at')->nullable()->comment('同步时间');
|
||||
$table->text('error_message')->nullable()->comment('错误信息');
|
||||
$table->timestamps();
|
||||
|
||||
// 添加外键约束
|
||||
$table->foreign('terminal_id')
|
||||
->references('id')
|
||||
->on('terminals')
|
||||
->onDelete('cascade');
|
||||
|
||||
// 添加索引
|
||||
$table->index('status', 'idx_terminal_sync_logs_status');
|
||||
$table->index('synced_at', 'idx_terminal_sync_logs_synced_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('terminal_sync_logs');
|
||||
}
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('terminals', function (Blueprint $table) {
|
||||
$table->string('mac_address', 17)->nullable()->unique()->after('ip_address')
|
||||
->comment('MAC地址 (AA:BB:CC:DD:EE:FF)');
|
||||
$table->string('scada_data_url', 500)->nullable()->after('diagram_url')
|
||||
->comment('OPC UA网关数据查询地址');
|
||||
$table->string('scada_tags_url', 500)->nullable()->after('scada_data_url')
|
||||
->comment('OPC UA网关点位定义查询地址');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('terminals', function (Blueprint $table) {
|
||||
$table->dropColumn(['mac_address', 'scada_data_url', 'scada_tags_url']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -38,8 +38,6 @@ class PermissionSeeder extends Seeder
|
||||
'terminal.create' => '创建终端',
|
||||
'terminal.update' => '编辑终端',
|
||||
'terminal.delete' => '删除终端',
|
||||
'terminal.sync' => '同步终端配置',
|
||||
|
||||
// 操作指引权限
|
||||
'guide.view' => '查看指引',
|
||||
'guide.create' => '创建指引',
|
||||
@@ -127,7 +125,6 @@ class PermissionSeeder extends Seeder
|
||||
'terminal.create',
|
||||
'terminal.update',
|
||||
'terminal.delete',
|
||||
'terminal.sync',
|
||||
|
||||
// 操作指引
|
||||
'guide.view',
|
||||
|
||||
@@ -38,13 +38,6 @@ class TerminalSeeder extends Seeder
|
||||
'ip_address' => $ipAddress,
|
||||
'station_id' => $beamline,
|
||||
'diagram_url' => 'https://ssrf.9z.work/scada/demo.html',
|
||||
'display_config' => [
|
||||
'resolution' => '3840x2160',
|
||||
'refresh_rate' => 60,
|
||||
'orientation' => 'landscape',
|
||||
'brightness' => 80,
|
||||
'touch_enabled' => true,
|
||||
],
|
||||
'is_online' => in_array($beamline, ['BL02U1', 'BL07U', 'BL08U', 'BL13U', 'BL15U']),
|
||||
'last_online_at' => in_array($beamline, ['BL02U1', 'BL07U', 'BL08U', 'BL13U', 'BL15U'])
|
||||
? now()
|
||||
|
||||
@@ -63,10 +63,6 @@ class TerminalResourceTest extends TestCase
|
||||
'ip_address' => '192.168.1.100',
|
||||
'station_id' => 1,
|
||||
'diagram_url' => 'https://example.com/diagram.html',
|
||||
'display_config' => [
|
||||
'resolution' => '1920x1080',
|
||||
'refresh_rate' => '60',
|
||||
],
|
||||
];
|
||||
|
||||
Livewire::test(CreateTerminal::class)
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Jobs\SyncTerminalConfigJob;
|
||||
use App\Models\Terminal;
|
||||
use App\Models\TerminalSyncLog;
|
||||
use App\Models\KnowledgeBase;
|
||||
use App\Models\TerminalPrompt;
|
||||
use App\Services\TerminalSyncService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TerminalSyncTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected TerminalSyncService $syncService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->syncService = app(TerminalSyncService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试单个终端同步
|
||||
*/
|
||||
public function test_can_sync_single_terminal(): void
|
||||
{
|
||||
Queue::fake();
|
||||
|
||||
// 创建测试终端
|
||||
$terminal = Terminal::factory()->create([
|
||||
'name' => '测试终端',
|
||||
'code' => 'TEST-001',
|
||||
]);
|
||||
|
||||
// 执行同步
|
||||
$log = $this->syncService->syncConfiguration($terminal);
|
||||
|
||||
// 断言同步日志已创建
|
||||
$this->assertInstanceOf(TerminalSyncLog::class, $log);
|
||||
$this->assertEquals('pending', $log->status);
|
||||
$this->assertEquals($terminal->id, $log->terminal_id);
|
||||
$this->assertNotNull($log->config_snapshot);
|
||||
|
||||
// 断言任务已加入队列
|
||||
Queue::assertPushed(SyncTerminalConfigJob::class, function ($job) use ($terminal, $log) {
|
||||
return $job->terminal->id === $terminal->id
|
||||
&& $job->log->id === $log->id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试配置快照包含完整信息
|
||||
*/
|
||||
public function test_config_snapshot_contains_complete_information(): void
|
||||
{
|
||||
// 创建终端及关联数据
|
||||
$terminal = Terminal::factory()->create([
|
||||
'name' => '测试终端',
|
||||
'code' => 'TEST-002',
|
||||
'ip_address' => '192.168.1.100',
|
||||
'station_id' => 1,
|
||||
'diagram_url' => 'https://example.com/diagram.html',
|
||||
'display_config' => ['resolution' => '1920x1080'],
|
||||
]);
|
||||
|
||||
// 创建知识库关联
|
||||
$kb1 = KnowledgeBase::factory()->create(['name' => '知识库1']);
|
||||
$kb2 = KnowledgeBase::factory()->create(['name' => '知识库2']);
|
||||
$terminal->knowledgeBases()->attach($kb1->id, ['priority' => 1]);
|
||||
$terminal->knowledgeBases()->attach($kb2->id, ['priority' => 2]);
|
||||
|
||||
// 创建提示词
|
||||
TerminalPrompt::factory()->create([
|
||||
'terminal_id' => $terminal->id,
|
||||
'prompt_template' => '你好,{user}',
|
||||
'variables' => ['user' => 'string'],
|
||||
]);
|
||||
|
||||
// 获取配置快照
|
||||
$snapshot = $this->syncService->getConfigSnapshot($terminal);
|
||||
|
||||
// 断言快照包含终端信息
|
||||
$this->assertArrayHasKey('terminal', $snapshot);
|
||||
$this->assertEquals('测试终端', $snapshot['terminal']['name']);
|
||||
$this->assertEquals('TEST-002', $snapshot['terminal']['code']);
|
||||
$this->assertEquals('192.168.1.100', $snapshot['terminal']['ip_address']);
|
||||
|
||||
// 断言快照包含知识库信息
|
||||
$this->assertArrayHasKey('knowledge_bases', $snapshot);
|
||||
$this->assertCount(2, $snapshot['knowledge_bases']);
|
||||
$this->assertEquals('知识库1', $snapshot['knowledge_bases'][0]['name']);
|
||||
$this->assertEquals(1, $snapshot['knowledge_bases'][0]['priority']);
|
||||
|
||||
// 断言快照包含提示词信息
|
||||
$this->assertArrayHasKey('prompt', $snapshot);
|
||||
$this->assertEquals('你好,{user}', $snapshot['prompt']['prompt_template']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试批量同步
|
||||
*/
|
||||
public function test_can_batch_sync_terminals(): void
|
||||
{
|
||||
Queue::fake();
|
||||
|
||||
// 创建多个终端
|
||||
$terminals = Terminal::factory()->count(3)->create();
|
||||
$terminalIds = $terminals->pluck('id')->toArray();
|
||||
|
||||
// 执行批量同步
|
||||
$logs = $this->syncService->batchSync($terminalIds);
|
||||
|
||||
// 断言创建了3个同步日志
|
||||
$this->assertCount(3, $logs);
|
||||
|
||||
// 断言每个终端都有同步日志
|
||||
foreach ($terminals as $terminal) {
|
||||
$this->assertDatabaseHas('terminal_sync_logs', [
|
||||
'terminal_id' => $terminal->id,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
}
|
||||
|
||||
// 断言任务已加入队列
|
||||
Queue::assertPushed(SyncTerminalConfigJob::class, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试同步状态更新
|
||||
*/
|
||||
public function test_can_update_sync_status(): void
|
||||
{
|
||||
$terminal = Terminal::factory()->create();
|
||||
$log = TerminalSyncLog::create([
|
||||
'terminal_id' => $terminal->id,
|
||||
'status' => 'pending',
|
||||
'config_snapshot' => [],
|
||||
]);
|
||||
|
||||
// 更新为同步中
|
||||
$this->syncService->updateSyncStatus($log, 'syncing');
|
||||
$this->assertEquals('syncing', $log->fresh()->status);
|
||||
|
||||
// 更新为已同步
|
||||
$this->syncService->updateSyncStatus($log, 'synced');
|
||||
$log->refresh();
|
||||
$this->assertEquals('synced', $log->status);
|
||||
$this->assertNotNull($log->synced_at);
|
||||
|
||||
// 更新为失败
|
||||
$this->syncService->updateSyncStatus($log, 'failed', '测试错误');
|
||||
$log->refresh();
|
||||
$this->assertEquals('failed', $log->status);
|
||||
$this->assertEquals('测试错误', $log->error_message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试同步任务成功执行
|
||||
*/
|
||||
public function test_sync_job_executes_successfully(): void
|
||||
{
|
||||
$terminal = Terminal::factory()->create();
|
||||
$log = TerminalSyncLog::create([
|
||||
'terminal_id' => $terminal->id,
|
||||
'status' => 'pending',
|
||||
'config_snapshot' => [],
|
||||
]);
|
||||
|
||||
// 执行任务
|
||||
$job = new SyncTerminalConfigJob($terminal, $log);
|
||||
$job->handle();
|
||||
|
||||
// 断言状态更新为已同步
|
||||
$log->refresh();
|
||||
$this->assertEquals('synced', $log->status);
|
||||
$this->assertNotNull($log->synced_at);
|
||||
$this->assertNull($log->error_message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试同步失败处理
|
||||
*/
|
||||
public function test_sync_job_handles_failure(): void
|
||||
{
|
||||
$terminal = Terminal::factory()->create();
|
||||
$log = TerminalSyncLog::create([
|
||||
'terminal_id' => $terminal->id,
|
||||
'status' => 'pending',
|
||||
'config_snapshot' => [],
|
||||
]);
|
||||
|
||||
$job = new SyncTerminalConfigJob($terminal, $log);
|
||||
|
||||
// 模拟失败(通过调用failed方法)
|
||||
$exception = new \Exception('测试同步失败');
|
||||
$job->failed($exception);
|
||||
|
||||
// 断言状态更新为失败
|
||||
$log->refresh();
|
||||
$this->assertEquals('failed', $log->status);
|
||||
$this->assertStringContainsString('测试同步失败', $log->error_message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试同步历史记录
|
||||
*/
|
||||
public function test_can_view_sync_history(): void
|
||||
{
|
||||
$terminal = Terminal::factory()->create();
|
||||
|
||||
// 创建多条同步记录
|
||||
TerminalSyncLog::create([
|
||||
'terminal_id' => $terminal->id,
|
||||
'status' => 'synced',
|
||||
'config_snapshot' => [],
|
||||
'synced_at' => now()->subHours(2),
|
||||
'created_at' => now()->subHours(2),
|
||||
]);
|
||||
|
||||
TerminalSyncLog::create([
|
||||
'terminal_id' => $terminal->id,
|
||||
'status' => 'failed',
|
||||
'config_snapshot' => [],
|
||||
'error_message' => '网络错误',
|
||||
'created_at' => now()->subHour(),
|
||||
]);
|
||||
|
||||
TerminalSyncLog::create([
|
||||
'terminal_id' => $terminal->id,
|
||||
'status' => 'synced',
|
||||
'config_snapshot' => [],
|
||||
'synced_at' => now(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
// 获取同步历史(应该按时间倒序)
|
||||
$logs = $terminal->syncLogs;
|
||||
|
||||
$this->assertCount(3, $logs);
|
||||
// 最新的记录应该在第一位
|
||||
$this->assertEquals('synced', $logs[0]->status);
|
||||
$this->assertEquals('failed', $logs[1]->status);
|
||||
$this->assertEquals('synced', $logs[2]->status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试获取最新同步日志
|
||||
*/
|
||||
public function test_can_get_latest_sync_log(): void
|
||||
{
|
||||
$terminal = Terminal::factory()->create();
|
||||
|
||||
// 创建多条同步记录
|
||||
TerminalSyncLog::create([
|
||||
'terminal_id' => $terminal->id,
|
||||
'status' => 'synced',
|
||||
'config_snapshot' => [],
|
||||
'created_at' => now()->subHours(2),
|
||||
]);
|
||||
|
||||
$latestLog = TerminalSyncLog::create([
|
||||
'terminal_id' => $terminal->id,
|
||||
'status' => 'pending',
|
||||
'config_snapshot' => [],
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
// 获取最新同步日志
|
||||
$result = $terminal->latestSyncLog;
|
||||
|
||||
$this->assertNotNull($result);
|
||||
$this->assertEquals($latestLog->id, $result->id);
|
||||
$this->assertEquals('pending', $result->status);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user