You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
10 KiB
214 lines
10 KiB
<?php
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Models\Company;
|
|
use App\Models\CompanyNews;
|
|
use App\Services\CompanyNews\NewsCollection;
|
|
use App\Services\CompanyNews\NewsEditor;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use RuntimeException;
|
|
|
|
class CompanyNewsController extends CommonController
|
|
{
|
|
public function dashboard(Request $r)
|
|
{
|
|
if ($error = $this->validateInput($r, ['page' => 'nullable|integer|min:1'])) return $error;
|
|
$start = now()->startOfYear();
|
|
$news = CompanyNews::query()->select('id', 'title', 'source_url', 'source_published_at')
|
|
->where('review_status', 'retained')
|
|
->where('source_published_at', '>=', $start)
|
|
->where('source_published_at', '<', $start->copy()->addYear())
|
|
->orderByDesc('source_published_at')->orderByDesc('id')
|
|
->paginate(8, ['*'], 'page', (int) $r->input('page', 1));
|
|
return $this->success($news);
|
|
}
|
|
|
|
public function index(Request $r)
|
|
{
|
|
if ($error = $this->validateInput($r, [
|
|
'company_id' => 'nullable|integer', 'keyword' => 'nullable|string|max:200',
|
|
'review_status' => 'nullable|in:retained,discarded',
|
|
'category_codes' => 'nullable|array|max:500', 'category_codes.*' => 'string|max:100',
|
|
'date_from' => 'nullable|date_format:Y-m-d',
|
|
'date_to' => 'nullable|date_format:Y-m-d'.($r->filled('date_from') ? '|after_or_equal:date_from' : ''),
|
|
])) return $error;
|
|
$q = CompanyNews::query()->select([
|
|
'id', 'company_id', 'company_name_snapshot', 'title', 'source_name', 'source_url',
|
|
'source_published_at', 'review_status', 'is_visible', 'version', 'first_seen_at', 'categories', 'emotion_type',
|
|
])->selectRaw("CASE WHEN content_html IS NULL OR content_html = '' THEN 0 ELSE 1 END as has_content")
|
|
->with('company:id,company_name');
|
|
if ($r->filled('company_id')) $q->where('company_id', $r->company_id);
|
|
if ($r->filled('keyword')) $q->where('title', 'like', '%'.$r->keyword.'%');
|
|
if ($r->input('review_status', 'retained') !== '') $q->where('review_status', $r->input('review_status', 'retained'));
|
|
if ($r->input('category_codes')) $q->where(function ($query) use ($r) {
|
|
foreach ($r->category_codes as $code) {
|
|
if (DB::connection()->getDriverName() === 'sqlite') {
|
|
$query->orWhereRaw("EXISTS (SELECT 1 FROM json_each(company_news.categories) WHERE json_extract(value, '$.code') = ?)", [$code]);
|
|
} else {
|
|
$query->orWhereJsonContains('categories', ['code' => $code]);
|
|
}
|
|
}
|
|
});
|
|
if ($r->filled('date_from')) $q->where('source_published_at', '>=', $r->date_from.' 00:00:00');
|
|
if ($r->filled('date_to')) $q->where('source_published_at', '<', Carbon::parse($r->date_to)->addDay()->startOfDay());
|
|
return $this->success($q->orderByDesc('source_published_at')->orderByDesc('id')->paginate(20));
|
|
}
|
|
|
|
public function categories()
|
|
{
|
|
$categories = [];
|
|
foreach (CompanyNews::select('id', 'categories')->cursor() as $news) {
|
|
$seen = [];
|
|
foreach ($news->categories ?? [] as $category) {
|
|
$code = (string) ($category['code'] ?? '');
|
|
if ($code === '' || isset($seen[$code])) continue;
|
|
$seen[$code] = true;
|
|
if (!isset($categories[$code])) $categories[$code] = ['code' => $code, 'name' => $category['name'] ?: $code, 'count' => 0];
|
|
$categories[$code]['count']++;
|
|
}
|
|
}
|
|
$rows = array_values($categories);
|
|
usort($rows, fn ($a, $b) => $b['count'] <=> $a['count'] ?: strcmp($a['code'], $b['code']));
|
|
return $this->success(['categories' => $rows, 'retained_categories' => json_decode(DB::table('company_news_settings')->where('id', 1)->value('retained_categories') ?? '[]', true)]);
|
|
}
|
|
|
|
public function autoSettings()
|
|
{
|
|
$settings = DB::table('company_news_settings')->where('id', 1)->first();
|
|
return $this->success([
|
|
'enabled' => (bool) $settings->auto_enabled, 'start_time' => $settings->auto_start_time,
|
|
'group_size' => (int) $settings->auto_group_size, 'timezone' => config('app.timezone'),
|
|
]);
|
|
}
|
|
|
|
public function saveAutoSettings(Request $r)
|
|
{
|
|
if ($error = $this->validateInput($r, [
|
|
'enabled' => 'required|boolean', 'start_time' => 'required|date_format:H:i',
|
|
'group_size' => 'required|integer|min:1|max:100',
|
|
])) return $error;
|
|
DB::table('company_news_settings')->where('id', 1)->update([
|
|
'auto_enabled' => $r->boolean('enabled'), 'auto_start_time' => $r->start_time,
|
|
'auto_group_size' => (int) $r->group_size, 'updated_by' => $this->getUserId(), 'updated_at' => now(),
|
|
]);
|
|
return $this->autoSettings();
|
|
}
|
|
|
|
public function saveSettings(Request $r)
|
|
{
|
|
if ($error = $this->validateInput($r, [
|
|
'retained_categories' => 'present|array|max:500', 'retained_categories.*' => 'required|string|max:100|distinct',
|
|
])) return $error;
|
|
DB::table('company_news_settings')->where('id', 1)->update([
|
|
'retained_categories' => json_encode(array_values($r->retained_categories)),
|
|
'updated_by' => $this->getUserId(), 'updated_at' => now(),
|
|
]);
|
|
return $this->success('分类设置已保存,从新建采集批次开始生效');
|
|
}
|
|
|
|
public function companies(Request $r)
|
|
{
|
|
if ($error = $this->validateInput($r, ['keyword' => 'nullable|string|max:100', 'page' => 'nullable|integer|min:1'])) return $error;
|
|
return $this->success(Company::query()->select('id', 'company_name')
|
|
->whereHas('qccAccount', fn ($q) => $q->whereIn('status', ['selected', 'occupied']))
|
|
->where('company_name', 'like', '%'.mb_substr((string) $r->input('keyword', ''), 0, 100).'%')
|
|
->orderBy('company_name')->orderBy('id')->paginate(30, ['*'], 'page', (int) $r->input('page', 1)));
|
|
}
|
|
|
|
public function show(Request $r)
|
|
{
|
|
if ($error = $this->validateInput($r, ['id' => 'required|integer|min:1'])) return $error;
|
|
$news = CompanyNews::with('company:id,company_name')->find($r->input('id'));
|
|
if (!$news) return $this->fail([404, '新闻不存在']);
|
|
return $this->success($news);
|
|
}
|
|
|
|
public function save(Request $r, NewsEditor $editor)
|
|
{
|
|
if ($error = $this->validateInput($r, [
|
|
'id' => 'required|integer', 'version' => 'required|integer|min:1',
|
|
'title' => 'required|string|max:2000', 'summary' => 'nullable|string|max:10000',
|
|
'content_html' => 'nullable|string|max:2000000',
|
|
])) return $error;
|
|
try {
|
|
return $this->success($editor->save($r->id, $r->version, $r->only('title', 'summary', 'content_html'), $this->getUserId()));
|
|
} catch (RuntimeException $e) { return $this->fail([409, $e->getMessage()]); }
|
|
}
|
|
|
|
public function review(Request $r, NewsEditor $editor)
|
|
{
|
|
if ($error = $this->validateInput($r, [
|
|
'id' => 'required|integer', 'version' => 'required|integer|min:1',
|
|
'review_status' => 'required|in:retained,discarded',
|
|
'review_note' => 'nullable|required_if:review_status,discarded|string|max:2000',
|
|
])) return $error;
|
|
try {
|
|
return $this->success($editor->review($r->id, $r->version, $r->review_status, $r->review_note, $this->getUserId()));
|
|
} catch (RuntimeException $e) { return $this->fail([409, $e->getMessage()]); }
|
|
}
|
|
|
|
public function reviewMany(Request $r, NewsEditor $editor)
|
|
{
|
|
if ($error = $this->validateInput($r, [
|
|
'items' => 'required|array|min:1|max:100', 'items.*.id' => 'required|integer|distinct',
|
|
'items.*.version' => 'required|integer|min:1',
|
|
'review_status' => 'required|in:retained,discarded',
|
|
'review_note' => 'nullable|required_if:review_status,discarded|string|max:2000',
|
|
])) return $error;
|
|
try {
|
|
DB::transaction(function () use ($r, $editor) {
|
|
// Deterministic row-lock order avoids deadlocks between overlapping selections.
|
|
foreach (collect($r->items)->sortBy('id') as $item) {
|
|
$editor->review($item['id'], $item['version'], $r->review_status, $r->review_note, $this->getUserId());
|
|
}
|
|
});
|
|
return $this->success('批量审核完成,所有新闻仍不展示');
|
|
} catch (RuntimeException $e) { return $this->fail([409, $e->getMessage()]); }
|
|
}
|
|
|
|
public function logs(Request $r)
|
|
{
|
|
if ($error = $this->validateInput($r, ['id' => 'required|integer|min:1'])) return $error;
|
|
return $this->success(DB::table('company_news_logs')->where('news_id', $r->input('id'))->orderByDesc('id')->paginate(20));
|
|
}
|
|
|
|
public function start(Request $r, NewsCollection $collector)
|
|
{
|
|
if ($error = $this->validateInput($r, ['retry_of' => 'nullable|integer|min:1'])) return $error;
|
|
try {
|
|
return $this->success($collector->start($this->getUserId(), $r->filled('retry_of') ? (int) $r->retry_of : null));
|
|
} catch (RuntimeException $e) { return $this->fail([409, $e->getMessage()]); }
|
|
}
|
|
|
|
public function latest(NewsCollection $collector)
|
|
{
|
|
$id = DB::table('company_news_control')->where('id', 1)->value('active_batch_id')
|
|
?: DB::table('company_news_batches')->max('id');
|
|
return $this->success(['batch' => $id ? $collector->summary($id) : null, 'configuration_error' => $collector->configurationError()]);
|
|
}
|
|
|
|
public function batches(NewsCollection $collector)
|
|
{
|
|
$rows = DB::table('company_news_batches')->orderByDesc('id')->paginate(10);
|
|
$rows->setCollection($rows->getCollection()->map(fn ($b) => $collector->summary($b->id)));
|
|
return $this->success($rows);
|
|
}
|
|
|
|
public function tasks(Request $r)
|
|
{
|
|
if ($error = $this->validateInput($r, ['batch_id' => 'required|integer|min:1'])) return $error;
|
|
$q = DB::table('company_news_tasks')->where('batch_id', $r->input('batch_id'));
|
|
if ($r->input('status') === 'failed') $q->where('status', 'failed');
|
|
return $this->success($q->orderBy('company_id')->paginate(20));
|
|
}
|
|
|
|
private function validateInput(Request $r, array $rules)
|
|
{
|
|
$v = Validator::make($r->all(), $rules);
|
|
return $v->fails() ? $this->fail([422, $v->errors()->first()]) : null;
|
|
}
|
|
}
|