问卷 新闻

master
lion 19 hours ago
parent ba174c5603
commit f0a571208d

@ -6,8 +6,8 @@ use Illuminate\Console\Command;
class CollectCompanyNews extends Command
{
protected $signature = 'qcc:collect-news {--limit=1000 : 单轮最多处理企业数} {--watch : 常驻等待并处理后台新建批次}';
protected $description = '依次执行后台已创建的新闻采集任务,不更改企业户状态';
protected $signature = 'qcc:collect-news {--limit= : 本轮企业数上限,默认使用批次的每组企业数} {--watch : 常驻等待并处理后台新建批次}';
protected $description = '检查每日自动采集计划,并按组执行企业新闻采集任务';
public function handle(NewsCollection $service): int
{
@ -15,14 +15,14 @@ class CollectCompanyNews extends Command
$this->error($error);
return self::FAILURE;
}
$limit = max(1, min(10000, (int) $this->option('limit')));
$limit = $this->option('limit') === null ? null : max(1, min(100, (int) $this->option('limit')));
$count = 0;
do {
try {
while ($count < $limit && $service->runOne()) {
$count++;
$this->line(now()->toDateTimeString()." 已处理 {$count} 个企业任务");
}
$batch = $service->startDue();
if ($batch) $this->info('自动创建批次 #'.$batch->id.',企业数 '.$batch->total);
$count = $service->runGroup($limit);
if ($count) $this->line(now()->toDateTimeString()." 本组已处理 {$count} 个企业任务");
} catch (\Throwable $e) {
if (!$this->option('watch')) throw $e;
report($e);

@ -15,8 +15,8 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
// 仅消费人工创建的新闻批次,不自动发起新的采集。数据库租约保证跨进程串行。
$schedule->command('qcc:collect-news')->everyMinute()->runInBackground();
// 检查自动采集时间并消费一组企业;数据库锁及日期唯一键防止重复批次。
$schedule->command('qcc:collect-news')->everyMinute()->withoutOverlapping(120)->runInBackground();
// 发送消息
$schedule->command("sms:notification")->everyMinute();

@ -22,7 +22,7 @@ class CompanyNewsController extends CommonController
->where('source_published_at', '>=', $start)
->where('source_published_at', '<', $start->copy()->addYear())
->orderByDesc('source_published_at')->orderByDesc('id')
->paginate(6, ['*'], 'page', (int) $r->input('page', 1));
->paginate(8, ['*'], 'page', (int) $r->input('page', 1));
return $this->success($news);
}
@ -75,6 +75,28 @@ class CompanyNewsController extends CommonController
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, [

@ -25,12 +25,17 @@ class NewsCollection
return null;
}
public function start(?int $actor, ?int $retryOf = null): object
public function start(?int $actor, ?int $retryOf = null, bool $scheduled = false): ?object
{
if ($error = $this->configurationError()) throw new RuntimeException($error);
return DB::transaction(function () use ($actor, $retryOf) {
return DB::transaction(function () use ($actor, $retryOf, $scheduled) {
$control = DB::table('company_news_control')->where('id', 1)->lockForUpdate()->first();
$settings = DB::table('company_news_settings')->where('id', 1)->lockForUpdate()->first();
$today = now()->toDateString();
if ($scheduled && (!$settings->auto_enabled || now()->format('H:i') < $settings->auto_start_time
|| DB::table('company_news_batches')->where('scheduled_date', $today)->exists())) return null;
if ($control->active_batch_id) {
if ($scheduled) return null;
throw new RuntimeException('已有采集批次运行,请等待完成后再获取更新');
}
$query = Company::query()->whereHas('qccAccount', fn ($q) => $q->whereIn('status', ['selected', 'occupied']))->orderBy('id');
@ -44,7 +49,9 @@ class NewsCollection
$now = now();
$id = DB::table('company_news_batches')->insertGetId([
'created_by' => $actor, 'retry_of' => $retryOf, 'total' => $companies->count(),
'retained_categories' => DB::table('company_news_settings')->where('id', 1)->value('retained_categories') ?? '[]',
'retained_categories' => $settings->retained_categories ?? '[]',
'scheduled_date' => $scheduled ? $today : null,
'group_size' => max(1, min(100, (int) $settings->auto_group_size)),
'status' => $companies->isEmpty() ? 'completed' : 'running',
'finished_at' => $companies->isEmpty() ? $now : null, 'created_at' => $now, 'updated_at' => $now,
]);
@ -72,16 +79,37 @@ class NewsCollection
$batch->processed = $batch->success + $batch->failed;
$batch->inserted = (int) (clone $tasks)->sum('inserted_count');
$batch->invalid = (int) (clone $tasks)->sum('invalid_count');
$batch->group_total = (int) ceil($batch->total / max(1, $batch->group_size));
$batch->group_processed = min($batch->group_total, (int) floor($batch->processed / max(1, $batch->group_size)));
if ($batch->processed === $batch->total) $batch->group_processed = $batch->group_total;
$batch->progress = $batch->total ? (int) floor($batch->processed * 100 / $batch->total) : 100;
$batch->current_company = (clone $tasks)->where('status', 'processing')->value('company_name');
return $batch;
}
public function runOne(): bool
public function startDue(): ?object
{
$claim = DB::transaction(function () {
return $this->start(null, null, true);
}
public function runGroup(?int $limit = null): int
{
$id = DB::table('company_news_control')->where('id', 1)->value('active_batch_id');
if (!$id) return 0;
$batch = $this->summary($id);
$size = max(1, (int) $batch->group_size);
$remaining = $size - ($batch->processed % $size);
if ($limit !== null) $remaining = min($remaining, max(1, $limit));
$count = 0;
while ($count < $remaining && $this->runOne($id)) $count++;
return $count;
}
public function runOne(?int $expectedBatchId = null): bool
{
$claim = DB::transaction(function () use ($expectedBatchId) {
$control = DB::table('company_news_control')->where('id', 1)->lockForUpdate()->first();
if (!$control->active_batch_id || ($control->lease_until && $control->lease_until > now()->toDateTimeString())) {
if (($expectedBatchId && $control->active_batch_id != $expectedBatchId) || !$control->active_batch_id || ($control->lease_until && $control->lease_until > now()->toDateTimeString())) {
return null;
}
$batchId = $control->active_batch_id;
@ -97,8 +125,9 @@ class NewsCollection
}
$token = (string) Str::uuid();
DB::table('company_news_control')->where('id', 1)->update(['worker_token' => $token, 'lease_until' => now()->addMinutes(5)]);
$task->request_ref = 'news-'.$task->id.'-'.$token;
DB::table('company_news_tasks')->where('id', $task->id)->update([
'status' => 'processing', 'started_at' => now(), 'request_ref' => 'news-'.$task->id.'-'.$token, 'updated_at' => now(),
'status' => 'processing', 'started_at' => now(), 'request_ref' => $task->request_ref, 'updated_at' => now(),
]);
return [$task, $token];
});
@ -116,7 +145,7 @@ class NewsCollection
if (!trim((string) $company->credit_code)) {
throw new RuntimeException('企业缺少统一社会信用代码');
}
// Deliberately bypass QccCallService: this operation must NEVER change account status.
// Fetch directly: occupation is recorded only for a successful response containing business data.
$response = app(YuanheRepository::class)->enterprisePackInfo([
'creditCode' => $company->credit_code, 'enterpriseName' => $company->company_name,
], 60);
@ -133,6 +162,8 @@ class NewsCollection
} catch (Throwable $e) {
$error = mb_substr($e->getMessage(), 0, 2000);
}
// Provider success is independent from local news import success.
if ($this->hasBusinessData($response)) $this->markOccupied($task, $token);
try {
$this->persist($task, $token, $response, $error);
} catch (Throwable $e) {
@ -142,6 +173,31 @@ class NewsCollection
return true;
}
private function hasBusinessData(?array $response): bool
{
if (!in_array($response['code'] ?? null, [200, '200'], true) || !is_array($response['data'] ?? null)) return false;
$hasValue = function ($value) use (&$hasValue): bool {
if (is_array($value)) {
foreach ($value as $child) if ($hasValue($child)) return true;
return false;
}
return is_string($value) ? trim($value) !== '' : (is_numeric($value) && $value != 0);
};
return $hasValue($response['data']);
}
private function markOccupied(object $task, string $token): void
{
DB::transaction(function () use ($task, $token) {
$control = DB::table('company_news_control')->where('id', 1)->lockForUpdate()->first();
if ($control->worker_token !== $token) return;
DB::table('company_qcc_accounts')->where('company_id', $task->company_id)->where('status', 'selected')->update([
'status' => 'occupied', 'first_success_at' => now(), 'first_success_source' => 'company-news',
'first_success_ref' => $task->request_ref, 'unknown_note' => null, 'updated_at' => now(),
]);
});
}
private function persist(object $task, string $token, ?array $response, ?string $error): void
{
DB::transaction(function () use ($task, $token, $response, $error) {

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up()
{
Schema::table('company_news_settings', function (Blueprint $t) {
$t->boolean('auto_enabled')->default(false);
$t->string('auto_start_time', 5)->default('22:00');
$t->unsignedInteger('auto_group_size')->default(10);
});
Schema::table('company_news_batches', function (Blueprint $t) {
$t->date('scheduled_date')->nullable()->unique();
$t->unsignedInteger('group_size')->default(10);
});
}
public function down()
{
Schema::table('company_news_batches', function (Blueprint $t) {
$t->dropUnique(['scheduled_date']);
$t->dropColumn(['scheduled_date', 'group_size']);
});
Schema::table('company_news_settings', fn (Blueprint $t) => $t->dropColumn(['auto_enabled', 'auto_start_time', 'auto_group_size']));
}
};

@ -1,13 +1,13 @@
# 企业新闻舆情部署与维护
后台页面 `/companyNews/index` 支持新闻采集、公司和发布日期筛选、人工正文编辑、留存审核及批次记录。后台驾驶舱 `/home` 的校友动态通过需管理员登录的 `/api/admin/other/alumni-news` 读取本年度已留存新闻,每页 6 条,按原文发布日期倒序;该内部展示不依赖 `is_visible`。当前没有匿名公开展示接口。所有新闻默认 `is_visible=false`,保存与审核也不会开启展示。
后台页面 `/companyNews/index` 支持新闻采集、公司和发布日期筛选、人工正文编辑、留存审核及批次记录。后台驾驶舱 `/home` 的校友动态通过需管理员登录的 `/api/admin/other/alumni-news` 读取本年度已留存新闻,每页 8 条,按原文发布日期倒序;该内部展示不依赖 `is_visible`。当前没有匿名公开展示接口。所有新闻默认 `is_visible=false`,保存与审核也不会开启展示。
## 业务规则
- 点击“获取更新”创建一个全量候选和已占额企业快照,不受新闻列表筛选和分页影响;仅 `company_qcc_accounts.status` 为 `selected` 或 `occupied` 的企业进入批次,按公司 ID 顺序执行。
- 执行前再次检查企业是否仍为候选或已占额;不符合的任务记录失败/跳过,不调用元禾。
- 单次请求使用元禾 `enterprise/packInfo`,连接超时 10 秒、总超时 60 秒;只有外层 `code=200` 且 `data.news` 是数组或 null 才进行处理。null/空数组记成功、0 新增;缺字段、非数组记失败。
- 本功能直接调用 Repository,不调用 QccCallService;成功、失败、超时均不更新企业户状态。原来的 `qcc:verify-pack-info` 诊断命令仍有状态流转,不能用它代替本功能。
- 本功能直接调用 Repository:code=200 且 data 中存在非空业务内容(新闻、股东等)时,仅将 selected 转为 occupied,记录首次成功时间、来源和引用;已占额及首次成功记录不重复改写。空数据、失败、超时不改状态。占额记录独立于新闻入库事务,避免本地保存失败丢失上游成功事实。原来的 `qcc:verify-pack-info` 诊断命令仍有状态流转,不能用它代替本功能。
- 按企业、数据提供方及去重键建立唯一约束。去重键按 NewsId、Id、原文 URL 的优先级生成 SHA-256;来源标识必须跨次稳定。同一新闻关联不同公司分别保存。
- 重复采集只更新来源字段、原始 JSON 与最近采集信息,不覆盖标题、人工正文和人工留存结论;自动判断的记录会按本批次分类设置重新判断,始终不展示。不留存是逻辑状态,不删除记录。
- 原文 HTML 使用现有 TinyMCE 组件编辑,并通过服务端 HTMLPurifier 清理。来源 Content 独立保存,接口正文为空时不会伪造原文。
@ -42,6 +42,7 @@
php artisan migrate --path=database/migrations/2026_10_09_100000_create_company_news_tables.php --force
php artisan migrate --path=database/migrations/2026_10_09_100001_add_company_news_menu.php --force
php artisan migrate --path=database/migrations/2026_10_12_100000_add_company_news_category_settings.php --force
php artisan migrate --path=database/migrations/2026_10_12_110000_add_company_news_auto_collection.php --force
```
新增五张表:company_news、company_news_logs、company_news_batches、company_news_tasks、company_news_control。
@ -64,12 +65,12 @@ npm run build:prod -- --dest dist
php artisan qcc:collect-news --watch
```
此模式每 5 秒检查是否有新建批次,只运行新闻采集,不触发其他短信、邮件等调度任务。重启电脑、停止终端或更改元禾配置后需重启该进程。生产环境可使用宝塔守护进程/Supervisor 管理该命令,或使用下面已有的 Laravel scheduler 方式。
此模式每组完成后等待 5 秒,检查每日计划并处理下一组,只运行新闻采集,不触发其他短信、邮件等调度任务。重启电脑、停止终端或更改元禾配置后需重启该进程。生产环境可使用宝塔守护进程/Supervisor 管理该命令,或使用下面已有的 Laravel scheduler 方式。
复用服务器已有 Laravel scheduler,每分钟执行 `php artisan schedule:run`。无需新增 Redis 或修改全站 QUEUE_CONNECTION;任务和锁存储在数据库中。
本次 Kernel 增加 `qcc:collect-news`,每分钟在后台启动消费者。它仅处理管理员已经创建的批次,不会每分钟重新创建采集批次。页面点击后通常在下一分钟开始;关闭页面不影响执行。
本次 Kernel 增加 `qcc:collect-news`,每分钟在后台启动消费者。它检查自动设置并处理当前批次中的一组;每天开始时间到达后,当天最多自动创建一次批次。没有常驻进程时,每组完成后由下一分钟调度继续。页面点击后通常在下一分钟开始;关闭页面不影响执行。
如果服务器尚未设置 Laravel scheduler,可在宝塔计划任务配置每分钟执行,PHP 路径使用服务器实际安装的 CLI 路径:
@ -89,7 +90,19 @@ php artisan qcc:collect-news
php artisan qcc:collect-news --limit=20
```
命令不创建批次、不改企业户状态;没有任务时立即退出。1000 家为顺序执行,实际耗时取决于元禾响应时间,页面持续展示进度,不承诺 10 分钟完成。
命令会检查自动采集设置,单次最多完成一组;--limit 可进一步限制本轮处理量,不会超过批次每组企业数。没有到期计划或待处理任务时退出。1000 家顺序执行,每组 10 家为 100 组;每家公司返回 0 条或 900 多条新闻都算 1 个企业任务,新闻不截断,去重及分类规则与手动获取一致。
## 每日自动采集设置
页面“自动获取更新设置”提供开关、每天 HH:mm 开始时间、每组 1~100 家。默认关闭,预设 22:00、10 家,时区 Asia/Shanghai。先配置守护进程或 scheduler,再启用。
- 名单、每组数量、分类留存规则在创建批次时快照保存,中途修改只影响下次批次。
- scheduled_date 唯一键和控制行锁防止同一天创建重复自动批次。旧批次未结束时,新任务延期等待,不截断旧任务。
- 当天时间已过且尚未自动采集,会在下一次检查补建当天批次;不追补漏跑的历史日期。
- 关闭开关只停止新建自动批次,现有批次继续。手动批次不占用当天自动批次名额。
- watch 模式组间等待 5 秒;scheduler 模式组间等待下一次每分钟调度。失败可从页面单独重试。
- 单家新闻处理完成后再请求下一家,避免把 1000 家响应同时载入内存。耗时取决于响应及入库速度,不能保证 22:00 开始一定午夜前结束。
- 部署时等待活动批次结束,再停止旧进程、执行迁移、更新代码并重启。手动和自动获取均使用有数据即占额规则。
## 重复执行和故障恢复

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -54,6 +54,8 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () {
// 企业新闻管理:仅后台授权访问。
Route::prefix('company-news')->middleware('rbac:admin')->group(function () {
Route::get('auto-settings', [\App\Http\Controllers\Admin\CompanyNewsController::class, 'autoSettings']);
Route::post('auto-settings', [\App\Http\Controllers\Admin\CompanyNewsController::class, 'saveAutoSettings']);
Route::get('categories', [\App\Http\Controllers\Admin\CompanyNewsController::class, 'categories']);
Route::post('settings', [\App\Http\Controllers\Admin\CompanyNewsController::class, 'saveSettings']);
Route::get('index', [\App\Http\Controllers\Admin\CompanyNewsController::class, 'index']);

@ -27,9 +27,12 @@ class CompanyNewsWorkflowTest extends TestCase
});
Schema::create('company_qcc_accounts', function (Blueprint $t) {
$t->id(); $t->unsignedBigInteger('company_id'); $t->string('status'); $t->timestamps();
$t->dateTime('first_success_at')->nullable(); $t->string('first_success_source')->nullable();
$t->string('first_success_ref')->nullable(); $t->text('unknown_note')->nullable();
});
(require base_path('database/migrations/2026_10_09_100000_create_company_news_tables.php'))->up();
(require base_path('database/migrations/2026_10_12_100000_add_company_news_category_settings.php'))->up();
(require base_path('database/migrations/2026_10_12_110000_add_company_news_auto_collection.php'))->up();
}
protected function tearDown(): void
{
@ -77,7 +80,7 @@ class CompanyNewsWorkflowTest extends TestCase
$this->assertEquals('<p>人工正文</p>', $news->content_html); $this->assertEquals('discarded', $news->review_status);
$this->assertFalse($news->is_visible); $this->assertEquals(0, $service->summary($second->id)->inserted);
$this->assertEquals(1, $service->summary($batch->id)->inserted);
$this->assertEquals('selected', DB::table('company_qcc_accounts')->value('status'));
$this->assertEquals('occupied', DB::table('company_qcc_accounts')->value('status'));
$this->assertEquals(2, DB::table('company_news_logs')->count());
}
public function test_failure_does_not_stop_other_companies_or_change_account_states(): void
@ -182,7 +185,7 @@ class CompanyNewsWorkflowTest extends TestCase
Carbon::setTestNow('2026-10-12 12:00:00');
$this->company(1);
$items = [];
for ($i = 1; $i <= 8; $i++) $items[] = $this->news('current-'.$i, '2026-01-01 00:00:00');
for ($i = 1; $i <= 10; $i++) $items[] = $this->news('current-'.$i, '2026-01-01 00:00:00');
$items[] = $this->news('previous', '2025-12-31 23:59:59');
$items[] = $this->news('next', '2027-01-01 00:00:00');
$items[] = $this->news('discarded', '2026-10-12 00:00:00');
@ -192,7 +195,7 @@ class CompanyNewsWorkflowTest extends TestCase
$controller = new CompanyNewsController();
$first = $controller->dashboard(Request::create('/', 'GET', ['page' => 1]))->getData(true);
$second = $controller->dashboard(Request::create('/', 'GET', ['page' => 2]))->getData(true);
$this->assertEquals(8, $first['total']); $this->assertCount(6, $first['data']); $this->assertCount(2, $second['data']);
$this->assertEquals(10, $first['total']); $this->assertCount(8, $first['data']); $this->assertCount(2, $second['data']);
$this->assertGreaterThan($first['data'][1]['id'], $first['data'][0]['id']);
$this->assertEmpty(array_intersect(array_column($first['data'], 'id'), array_column($second['data'], 'id')));
$this->assertEquals(['id', 'title', 'source_url', 'source_published_at'], array_keys($first['data'][0]));
@ -202,6 +205,88 @@ class CompanyNewsWorkflowTest extends TestCase
$this->assertEquals(1, $controller->dashboard(Request::create('/'))->getData(true)['total']);
}
public function test_daily_schedule_runs_once_after_start_and_defers_while_active(): void
{
Carbon::setTestNow('2026-10-12 21:59:00');
$this->company(1); $this->provider([$this->response([]), $this->response([])]);
$service = new NewsCollection();
$this->assertNull($service->startDue());
DB::table('company_news_settings')->update(['auto_enabled' => true, 'auto_start_time' => '22:00', 'auto_group_size' => 2]);
$this->assertNull($service->startDue());
$manual = $service->start(1);
Carbon::setTestNow('2026-10-12 22:01:00');
$this->assertNull($service->startDue());
$service->runGroup();
$batch = $service->startDue();
$this->assertEquals('2026-10-12', $batch->scheduled_date);
$this->assertEquals(2, $batch->group_size);
$this->assertNull($service->startDue());
$service->runGroup();
$this->assertNull($service->startDue());
Carbon::setTestNow('2026-10-13 22:00:00');
$this->assertNotNull($service->startDue());
}
public function test_group_size_is_snapshotted_and_all_groups_continue_after_failure(): void
{
foreach ([1, 2, 3, 4, 5] as $id) $this->company($id);
DB::table('company_news_settings')->update(['auto_group_size' => 2]);
$this->provider([new RuntimeException('timeout'), $this->response([]), $this->response([]), $this->response([]), $this->response([])]);
$service = new NewsCollection(); $batch = $service->start(1);
DB::table('company_news_settings')->update(['auto_group_size' => 5]);
$this->assertEquals(2, $service->runGroup());
$this->assertEquals(1, $service->summary($batch->id)->group_processed);
$this->assertEquals(2, $service->runGroup());
$this->assertEquals(1, $service->runGroup());
$this->assertEquals(0, $service->runGroup());
$summary = $service->summary($batch->id);
$this->assertEquals(3, $summary->group_total);
$this->assertEquals(3, $summary->group_processed);
$this->assertEquals(5, $summary->processed);
$this->assertEquals(1, $summary->failed);
$this->assertEquals('completed', $summary->status);
}
public function test_auto_settings_validation_and_disable_do_not_cancel_active_batch(): void
{
$controller = new class extends CompanyNewsController { public function getUserId() { return 1; } };
$invalid = $controller->saveAutoSettings(Request::create('/', 'POST', ['enabled' => true, 'start_time' => '25:90', 'group_size' => 0]))->getData(true);
$this->assertEquals(422, $invalid['errcode']);
$controller->saveAutoSettings(Request::create('/', 'POST', ['enabled' => true, 'start_time' => '22:00', 'group_size' => 10]));
$this->company(1); $service = new NewsCollection(); $batch = $service->start(1);
$controller->saveAutoSettings(Request::create('/', 'POST', ['enabled' => false, 'start_time' => '22:00', 'group_size' => 10]));
$this->assertFalse($controller->autoSettings()->getData(true)['enabled']);
$this->assertEquals($batch->id, DB::table('company_news_control')->value('active_batch_id'));
}
public function test_business_data_marks_candidates_occupied_even_without_news_and_keeps_first_success(): void
{
$this->company(1); $this->company(2); $this->company(3); $this->company(4, 'occupied');
DB::table('company_qcc_accounts')->where('company_id', 4)->update(['first_success_ref' => 'original']);
$partners = ['code' => 200, 'data' => ['news' => [], 'partners' => [['StockName' => '股东名称']]]];
$empty = ['code' => 200, 'data' => ['news' => null, 'listedCompany' => ['detail' => null, 'mainIndicator' => []]]];
$this->provider([$partners, $empty, ['code' => 500, 'data' => ['partners' => [['StockName' => '股东']]]], $partners]);
$service = new NewsCollection(); $batch = $service->start(1); while ($service->runOne()) {}
$this->assertEquals(['occupied', 'selected', 'selected', 'occupied'], DB::table('company_qcc_accounts')->orderBy('company_id')->pluck('status')->all());
$this->assertStringStartsWith('news-', DB::table('company_qcc_accounts')->where('company_id', 1)->value('first_success_ref'));
$this->assertEquals('original', DB::table('company_qcc_accounts')->where('company_id', 4)->value('first_success_ref'));
$this->assertEquals(3, $service->summary($batch->id)->success);
$this->assertEquals(1, $service->summary($batch->id)->failed);
}
public function test_large_response_imports_every_news_and_deduplicates_on_repeat(): void
{
$this->company(1);
$news = []; for ($i = 0; $i < 901; $i++) $news[] = $this->news('bulk-'.$i);
$this->provider([$this->response($news), $this->response($news)]);
$service = new NewsCollection(); $first = $service->start(1); $service->runGroup();
$this->assertEquals(901, $service->summary($first->id)->inserted);
$second = $service->start(1); $service->runGroup();
$this->assertEquals(0, $service->summary($second->id)->inserted);
$this->assertEquals(901, CompanyNews::count());
$this->assertEquals('occupied', DB::table('company_qcc_accounts')->value('status'));
}
public function test_empty_batch_finishes_immediately(): void
{
$service = new NewsCollection(); $batch = $service->start(1);

Loading…
Cancel
Save