CaiWuXiTong/modules/EcommerceFinance/Repositories/DataWarningMirrorRepository.php
WuYongJie 561c42d830 feat: 店铺主体期间与爬虫数据逻辑
- 店铺维度报表按期初数据+系统主体+爬虫新主体生成主体期间,一行一期间,修复流程历史重复行
- 变更日当天归新主体、老主体失效显示变更日前一天;关店和其他非正常状态两列显示 -
- 新增店铺维度报表逻辑配置页(R1-R8)、主体商标数据逻辑配置页和预警页面
- 爬虫店铺主体按税号归一,首次抓到的新主体写入期间变更表并自动生成新期间
- 店铺基础信息、商标展示按爬虫优先;新增爬虫数据质量检查与预警处理
2026-10-03 13:28:08 +08:00

190 lines
6.8 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Repositories;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Carbon;
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
class DataWarningMirrorRepository
{
private readonly ConnectionInterface $connection;
public function __construct(GenericBusinessMirrorRepository $mirror)
{
$this->connection = $mirror->connection();
}
public function ready(): bool
{
return $this->connection->getSchemaBuilder()->hasTable('business_data_warnings');
}
/**
* 生成或更新一条预警。同 warning_key 重复触发时只更新风险描述和影响,
* 保留首次触发时间、责任人指派和处理留痕。
*
* @param array<string,mixed> $warning
*/
public function upsert(array $warning): void
{
$key = (string) $warning['warning_key'];
$existing = $this->connection->table('business_data_warnings')->where('warning_key', $key)->first();
if ($existing !== null) {
$this->connection->table('business_data_warnings')->where('warning_key', $key)->update([
'warning_type' => $warning['warning_type'],
'risk_level' => $warning['risk_level'],
'related_object' => $warning['related_object'] ?? null,
'risk_description' => $warning['risk_description'] ?? null,
'evidence' => $warning['evidence'] ?? null,
'impact' => $warning['impact'] ?? null,
'trigger_rule' => $warning['trigger_rule'] ?? null,
'updated_at' => now(),
]);
return;
}
$this->connection->table('business_data_warnings')->insert([
'warning_key' => $key,
'warning_number' => $this->nextWarningNumber(),
'warning_type' => $warning['warning_type'],
'risk_level' => $warning['risk_level'],
'related_object' => $warning['related_object'] ?? null,
'risk_description' => $warning['risk_description'] ?? null,
'evidence' => $warning['evidence'] ?? null,
'impact' => $warning['impact'] ?? null,
'trigger_rule' => $warning['trigger_rule'] ?? null,
'triggered_at' => now(),
'responsible_person' => $warning['responsible_person'] ?? null,
'due_at' => $warning['due_at'] ?? null,
'review_status' => 'pending',
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* 预警页面数据。按触发时间倒序,等待复审的排在前面。
*
* @param array<string,string> $filters
* @return array{data: array<int,array<string,mixed>>, total: int, pending: int}
*/
public function paginate(int $page, int $perPage, array $filters = []): array
{
if (! $this->ready()) {
return ['data' => [], 'total' => 0, 'pending' => 0];
}
$query = $this->connection->table('business_data_warnings');
foreach (['warning_type', 'risk_level', 'review_status'] as $exact) {
$value = trim((string) ($filters[$exact] ?? ''));
if ($value !== '') {
$query->where($exact, '=', $value);
}
}
foreach (['warning_number', 'related_object', 'responsible_person'] as $text) {
$value = trim((string) ($filters[$text] ?? ''));
if ($value !== '') {
$query->where($text, 'like', '%' . addcslashes($value, '%_\\') . '%');
}
}
$total = (clone $query)->count();
$pending = (clone $query)->where('review_status', '=', 'pending')->count();
$rows = $query
->orderByRaw("case when review_status = 'pending' then 0 else 1 end")
->orderByDesc('triggered_at')
->orderByDesc('id')
->forPage(max($page, 1), $perPage)
->get()
->map(static function (object $row): array {
$row = (array) $row;
$row['triggered_at'] = $row['triggered_at'] !== null
? Carbon::parse($row['triggered_at'])->format('Y-m-d H:i')
: null;
$row['reviewed_at'] = $row['reviewed_at'] !== null
? Carbon::parse($row['reviewed_at'])->format('Y-m-d H:i')
: null;
$row['due_at'] = $row['due_at'] !== null
? Carbon::parse($row['due_at'])->format('Y-m-d')
: null;
$row['review_status_label'] = match ((string) $row['review_status']) {
'pending' => '待复审',
'confirmed' => '已确认',
'rejected' => '已驳回',
default => (string) $row['review_status'],
};
return $row;
})
->all();
return ['data' => $rows, 'total' => $total, 'pending' => $pending];
}
/** @param array<int,array<string,mixed>> $warnings */
public function upsertMany(array $warnings): int
{
$count = 0;
foreach ($warnings as $warning) {
$this->upsert($warning);
$count++;
}
return $count;
}
public function find(int $id): ?array
{
if (! $this->ready()) {
return null;
}
$row = $this->connection->table('business_data_warnings')->where('id', $id)->first();
return $row === null ? null : (array) $row;
}
/**
* 人工复审:确认时保持系统按爬虫生成的结果,驳回时记录依据由后续
* 期间合并任务改回流程数据。
*/
public function review(int $id, string $status, string $reviewer, ?string $note, ?string $responsiblePerson = null): bool
{
if (! $this->ready()) {
return false;
}
$update = [
'review_status' => $status,
'reviewer' => $reviewer,
'reviewed_at' => now(),
'review_note' => $note,
'updated_at' => now(),
];
if ($responsiblePerson !== null && trim($responsiblePerson) !== '') {
$update['responsible_person'] = trim($responsiblePerson);
}
return $this->connection->table('business_data_warnings')->where('id', $id)->update($update) > 0;
}
private function nextWarningNumber(): string
{
$date = now()->format('Ymd');
$prefix = 'WARN-' . $date . '-';
$latest = $this->connection->table('business_data_warnings')
->where('warning_number', 'like', $prefix . '%')
->orderByDesc('warning_number')
->value('warning_number');
$sequence = $latest === null ? 0 : (int) substr((string) $latest, -3);
return $prefix . str_pad((string) ($sequence + 1), 3, '0', STR_PAD_LEFT);
}
}