问卷 新闻

master
lion 2 days ago
parent 7cd5fcf899
commit 9766c877a4

@ -17,7 +17,8 @@ class CompanyNewsController extends CommonController
{
if ($error = $this->validateInput($r, [
'company_id' => 'nullable|integer', 'keyword' => 'nullable|string|max:200',
'review_status' => 'nullable|in:pending,retained,discarded',
'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;
@ -28,12 +29,51 @@ class CompanyNewsController extends CommonController
->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->filled('review_status')) $q->where('review_status', $r->review_status);
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 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;
@ -67,7 +107,7 @@ class CompanyNewsController extends CommonController
{
if ($error = $this->validateInput($r, [
'id' => 'required|integer', 'version' => 'required|integer|min:1',
'review_status' => 'required|in:pending,retained,discarded',
'review_status' => 'required|in:retained,discarded',
'review_note' => 'nullable|required_if:review_status,discarded|string|max:2000',
])) return $error;
try {

@ -16,6 +16,7 @@ class QccEnterpriseAccountController extends CommonController
{
public function candidates(Request $request)
{
if ($error = $this->validateStudentFilters($request)) return $error;
$keyword = trim((string) $request->input('keyword', ''));
$status = $request->input('status');
$pageSize = min(max((int) $request->input('page_size', 20), 1), 100);
@ -26,23 +27,88 @@ class QccEnterpriseAccountController extends CommonController
->orWhere('credit_code', 'like', '%' . $keyword . '%');
});
}
$this->applyStudentFilters($query, $request);
$query->with(['users' => $this->studentRelation()]);
if ($status === 'none') {
$query->doesntHave('qccAccount');
} elseif (in_array($status, [CompanyQccAccount::STATUS_SELECTED, CompanyQccAccount::STATUS_OCCUPIED, CompanyQccAccount::STATUS_UNKNOWN], true)) {
$query->whereHas('qccAccount', fn($relation) => $relation->where('status', $status));
}
return $this->success($query->orderByDesc('id')->paginate($pageSize));
return $this->studentResponse($query->orderByDesc('id')->paginate($pageSize));
}
public function index(Request $request)
{
if ($error = $this->validateStudentFilters($request)) return $error;
$status = $request->input('status');
$pageSize = min(max((int) $request->input('page_size', 20), 1), 100);
$query = CompanyQccAccount::query()->with('company:id,company_name,credit_code');
$query = CompanyQccAccount::query()->with(['company:id,company_name,credit_code', 'company.users' => $this->studentRelation()]);
$query->whereHas('company', function ($company) use ($request) {
$keyword = trim((string) $request->input('keyword', ''));
if ($keyword !== '') $company->where(fn ($q) => $q->where('company_name', 'like', '%'.$keyword.'%')->orWhere('credit_code', 'like', '%'.$keyword.'%'));
$this->applyStudentFilters($company, $request);
});
if (in_array($status, [CompanyQccAccount::STATUS_SELECTED, CompanyQccAccount::STATUS_OCCUPIED, CompanyQccAccount::STATUS_UNKNOWN], true)) {
$query->where('status', $status);
}
return $this->success($query->orderByDesc('id')->paginate($pageSize));
return $this->studentResponse($query->orderByDesc('id')->paginate($pageSize));
}
private function studentResponse($page)
{
foreach ($page as $row) {
$company = $row instanceof Company ? $row : $row->company;
foreach ($company ? $company->users : [] as $user) {
// Avoid unrelated appointment/company accessors and extra queries during serialization.
$user->setAppends([]);
foreach ($user->courseSigns as $sign) {
$sign->setAppends([]);
if ($sign->course) $sign->course->setAppends([]);
}
}
}
return $this->success($page);
}
private function validateStudentFilters(Request $request)
{
$validator = Validator::make($request->all(), [
'keyword' => 'nullable|string|max:200', 'user_name' => 'nullable|string|max:100',
'is_schoolmate' => 'nullable|in:0,1', 'course_type_id' => 'nullable|integer|min:1',
'course_name' => 'nullable|string|max:200',
]);
return $validator->fails() ? $this->fail([ResponseCode::ERROR_PARAMETER, $validator->errors()->first()]) : null;
}
private function studentRelation(): \Closure
{
return function ($users) {
$users->select('id', 'company_id', 'no', 'name', 'username', 'is_schoolmate', 'company_position')
->whereHas('courseSigns', fn ($sign) => $sign->where('status', 1))
->with(['courseSigns' => fn ($sign) => $sign->select('id', 'user_id', 'course_id', 'status')
->where('status', 1)->with('course:id,name,type,start_date')]);
};
}
private function applyStudentFilters($companies, Request $request): void
{
if (!$request->filled('user_name') && !$request->filled('is_schoolmate') && !$request->filled('course_type_id') && !$request->filled('course_name')) return;
$companies->whereHas('users', function ($user) use ($request) {
if ($request->filled('user_name')) {
$name = trim($request->user_name);
$user->where(fn ($q) => $q->where('name', 'like', '%'.$name.'%')->orWhere('username', 'like', '%'.$name.'%'));
}
if ($request->filled('is_schoolmate')) $user->where('is_schoolmate', $request->is_schoolmate);
$user->whereHas('courseSigns', function ($sign) use ($request) {
$sign->where('status', 1);
if ($request->filled('course_type_id') || $request->filled('course_name')) {
$sign->whereHas('course', function ($course) use ($request) {
if ($request->filled('course_type_id')) $course->where('type', $request->course_type_id);
if ($request->filled('course_name')) $course->where('name', 'like', '%'.trim($request->course_name).'%');
});
}
});
});
}
public function select(Request $request)

@ -44,6 +44,7 @@ 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') ?? '[]',
'status' => $companies->isEmpty() ? 'completed' : 'running',
'finished_at' => $companies->isEmpty() ? $now : null, 'created_at' => $now, 'updated_at' => $now,
]);
@ -152,12 +153,13 @@ class NewsCollection
$invalid = 0;
$issues = [];
$items = $error ? [] : ($response['data']['news'] ?? []);
$retainedCategories = json_decode(DB::table('company_news_batches')->where('id', $task->batch_id)->value('retained_categories') ?? '[]', true);
foreach ($items as $index => $item) {
try {
if (!is_array($item)) {
throw new InvalidArgumentException('新闻不是对象');
}
$inserted += (int) app(NewsImporter::class)->store($task, $item);
$inserted += (int) app(NewsImporter::class)->store($task, $item, $retainedCategories);
} catch (InvalidArgumentException $e) {
$invalid++;
if (count($issues) < 10) {

@ -30,6 +30,7 @@ class NewsEditor
public function review(int $id, int $version, string $status, ?string $note, ?int $actor): CompanyNews
{
if (!in_array($status, ['retained', 'discarded'], true)) throw new RuntimeException('不支持的留存状态');
return DB::transaction(function () use ($id, $version, $status, $note, $actor) {
$news = $this->locked($id, $version);
$from = $news->review_status;

@ -7,7 +7,7 @@ use InvalidArgumentException;
class NewsImporter
{
public function store(object $task, array $item): bool
public function store(object $task, array $item, array $retainedCategories = []): bool
{
$title = $this->text($item, 'Title');
if ($title === '') {
@ -35,7 +35,7 @@ class NewsImporter
foreach ($codes as $i => $code) {
$categories[] = ['code' => $code, 'name' => $names[$i] ?? ''];
}
$news = CompanyNews::firstOrNew([
$news = CompanyNews::lockForUpdate()->firstOrNew([
'company_id' => $task->company_id, 'provider' => 'yuanhe-qcc',
'dedup_key' => hash('sha256', $identity),
]);
@ -44,10 +44,18 @@ class NewsImporter
$news->fill([
'company_name_snapshot' => $task->company_name, 'title' => $title,
'first_seen_at' => now(), 'first_task_id' => $task->id,
'review_status' => 'pending', 'is_visible' => false,
'review_status' => 'discarded', 'is_visible' => false,
]);
}
// Never write editorial fields, review decisions or visibility on a repeated import.
// Re-evaluate automatic decisions, preserving manual review and editorial content.
if ($new || !$news->reviewed_at) {
$retained = count(array_intersect($codes, $retainedCategories)) > 0;
$news->review_status = $retained ? 'retained' : 'discarded';
$news->review_note = $retained ? '自动留存:命中分类设置' : '自动不留存:未命中分类设置';
$news->is_visible = false;
$news->visible_at = null;
if (!$new) $news->version++;
}
$news->fill([
'external_id' => $id ?: null, 'external_news_id' => $newsId ?: null,
'source_title' => $title, 'source_name' => $this->text($item, 'Source'),

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up()
{
Schema::create('company_news_settings', function (Blueprint $t) {
$t->unsignedInteger('id')->primary();
$t->json('retained_categories');
$t->unsignedBigInteger('updated_by')->nullable();
$t->timestamps();
});
DB::table('company_news_settings')->insert(['id' => 1, 'retained_categories' => '[]', 'created_at' => now(), 'updated_at' => now()]);
Schema::table('company_news_batches', fn (Blueprint $t) => $t->json('retained_categories')->nullable());
DB::table('company_news')->where('review_status', 'pending')->update([
'review_status' => 'discarded', 'review_note' => '待审核状态已取消,等待下次采集按分类设置判断',
'reviewed_at' => null, 'reviewed_by' => null, 'is_visible' => false, 'visible_at' => null,
'version' => DB::raw('version + 1'), 'updated_at' => now(),
]);
}
public function down()
{
Schema::table('company_news_batches', fn (Blueprint $t) => $t->dropColumn('retained_categories'));
Schema::dropIfExists('company_news_settings');
}
};

@ -9,12 +9,21 @@
- 单次请求使用元禾 `enterprise/packInfo`,连接超时 10 秒、总超时 60 秒;只有外层 `code=200` 且 `data.news` 是数组或 null 才进行处理。null/空数组记成功、0 新增;缺字段、非数组记失败。
- 本功能直接调用 Repository,不调用 QccCallService;成功、失败、超时均不更新企业户状态。原来的 `qcc:verify-pack-info` 诊断命令仍有状态流转,不能用它代替本功能。
- 按企业、数据提供方及去重键建立唯一约束。去重键按 NewsId、Id、原文 URL 的优先级生成 SHA-256;来源标识必须跨次稳定。同一新闻关联不同公司分别保存。
- 重复采集只更新来源字段、原始 JSON 与最近采集信息,不覆盖标题、人工正文、审核结论或展示状态。不留存是逻辑状态,不删除记录。
- 重复采集只更新来源字段、原始 JSON 与最近采集信息,不覆盖标题、人工正文和人工留存结论;自动判断的记录会按本批次分类设置重新判断,始终不展示。不留存是逻辑状态,不删除记录。
- 原文 HTML 使用现有 TinyMCE 组件编辑,并通过服务端 HTMLPurifier 清理。来源 Content 独立保存,接口正文为空时不会伪造原文。
- 原文抓取仅预留字段,本期不自动访问来源链接抓取正文。
- 新闻列表日期筛选依据原文 PublishTime,按应用时区保存与筛选,结束日期包含当天全部时间。
- 单条新闻字段错误会记录序号与错误,其余正常条目继续保存;该企业任务记失败,以便核查和重试。成功/失败按企业计数,新增加总实际插入行数。
## 分类筛选与自动留存
- 分类下拉列出所有已入库的分类,按分类代码去重并统计全部新闻数量,同一新闻在同一分类只计一次;支持输入搜索、多选,命中任意选项即可。
- 页面默认显示已留存,状态只有全部、已留存、不留存。
- “分类留存设置”保存到 company_news_settings。未选择分类时全部不留存;无分类或新出现但未选中的分类不留存。
- 新建批次将设置保存到 company_news_batches.retained_categories,中途改设置仅影响后续批次。
- 新采集和重复采集的自动处理记录命中任意已选分类即留存,其他不留存;reviewed_at 非空表示已人工决定,重复采集保留人工决定。
- 迁移将历史待审核新闻转为不留存,后续采集再应用设置;已有人工留存结果不变。设置保存不回溯全量历史新闻。
## 企业下拉查询
公司筛选仅返回企业户中 selected、occupied 的企业,不返回 unknown 或未纳入的企业。接口按企业名称、ID 排序,每页 30 家;前端滚动触底加载下一页,输入关键词时从第一页重新搜索。“获取更新”采集全部候选和已占额企业,不受当前公司筛选或下拉分页影响。
@ -27,11 +36,12 @@
## 上线步骤
部署本次 PHP 源码后,在项目根目录执行这两条迁移(不包含工作区其他功能的迁移):
部署本次 PHP 源码后,在项目根目录执行以下迁移(不包含工作区其他功能的迁移):
```bash
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
```
新增五张表:company_news、company_news_logs、company_news_batches、company_news_tasks、company_news_control。
@ -89,7 +99,7 @@ php artisan qcc:collect-news --limit=20
- 网络超时不能证明元禾未收到请求。对方没有提供幂等协议时,无法保证外部调用严格只发生一次;“仅重试失败企业”由管理员明确启动新批次,并重新检查候选或已占额状态。
- 某个任务保存失败,其事务回滚后独立记失败;数据库整体不可用时命令退出,调度恢复后按租约继续。
- 页面显示“等待后台调度”长时间无进展:检查宝塔 scheduler、PHP CLI 版本、任务日志和 active_batch_id。不要通过删除控制行或随意清锁恢复,这可能破坏防重入机制。
- 不留存记录必须保留,否则后续采集又会变成新的待审核记录。
- 不留存记录必须保留,否则后续采集又会变成新的记录。
## 验证命令

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

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

@ -52,6 +52,8 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () {
// 企业新闻:仅后台授权访问;没有对外展示接口。
Route::prefix('company-news')->middleware('rbac:admin')->group(function () {
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']);
Route::get('companies', [\App\Http\Controllers\Admin\CompanyNewsController::class, 'companies']);
Route::get('show', [\App\Http\Controllers\Admin\CompanyNewsController::class, 'show']);

@ -29,6 +29,7 @@ class CompanyNewsWorkflowTest extends TestCase
$t->id(); $t->unsignedBigInteger('company_id'); $t->string('status'); $t->timestamps();
});
(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();
}
protected function tearDown(): void
{
@ -66,7 +67,7 @@ class CompanyNewsWorkflowTest extends TestCase
$service = new NewsCollection(); $batch = $service->start(1); $service->runOne();
$news = CompanyNews::first();
$this->assertEquals(1, CompanyNews::count()); $this->assertFalse($news->is_visible);
$this->assertEquals('pending', $news->review_status); $this->assertEquals('00000', $news->categories[0]['code']);
$this->assertEquals('discarded', $news->review_status); $this->assertEquals('00000', $news->categories[0]['code']);
$this->assertNull($news->content_html);
$editor = new NewsEditor();
$news = $editor->save($news->id, $news->version, ['title' => '人工标题', 'content_html' => '<p>人工正文</p>'], 7);
@ -141,6 +142,41 @@ class CompanyNewsWorkflowTest extends TestCase
$this->assertEquals('success', DB::table('company_news_tasks')->where('company_id', 1)->value('status'));
$this->assertEquals('failed', DB::table('company_news_tasks')->where('company_id', 2)->value('status'));
}
public function test_category_rules_snapshot_counts_filter_and_manual_override(): void
{
$this->company(1);
DB::table('company_news_settings')->update(['retained_categories' => '["12014"]']);
$other = $this->news('other'); $other['Category'] = '14004'; $other['CategoryDesc'] = '荣誉奖项';
$this->provider([$this->response([$this->news(), $other]), $this->response([$this->news(), $other])]);
$service = new NewsCollection(); $service->start(1);
DB::table('company_news_settings')->update(['retained_categories' => '[]']);
$service->runOne();
$this->assertEquals('retained', CompanyNews::where('external_news_id', 'news-1')->value('review_status'));
$this->assertEquals('discarded', CompanyNews::where('external_news_id', 'other')->value('review_status'));
$manual = CompanyNews::where('external_news_id', 'other')->first();
(new NewsEditor())->review($manual->id, $manual->version, 'retained', '', 1);
$controller = new CompanyNewsController();
$this->assertEquals(2, $controller->index(Request::create('/'))->getData(true)['total']);
$filtered = $controller->index(Request::create('/', 'GET', ['category_codes' => ['00000', '12014']]))->getData(true);
$this->assertEquals(1, $filtered['total']);
$categories = $controller->categories()->getData(true)['categories'];
$this->assertCount(3, $categories);
$this->assertEquals([1, 1, 1], array_column($categories, 'count'));
$service->start(1); $service->runOne();
$this->assertEquals('discarded', CompanyNews::where('external_news_id', 'news-1')->value('review_status'));
$this->assertEquals('retained', $manual->fresh()->review_status);
$this->assertEquals(0, CompanyNews::where('is_visible', true)->count());
}
public function test_category_settings_accept_empty_selection_and_reject_pending_review(): void
{
$controller = new class extends CompanyNewsController { public function getUserId() { return 1; } };
$controller->saveSettings(Request::create('/', 'POST', ['retained_categories' => ['00000', '12014']]));
$this->assertEquals(['00000', '12014'], $controller->categories()->getData(true)['retained_categories']);
$controller->saveSettings(Request::create('/', 'POST', ['retained_categories' => []]));
$this->assertEquals([], $controller->categories()->getData(true)['retained_categories']);
$result = $controller->review(Request::create('/', 'POST', ['id' => 1, 'version' => 1, 'review_status' => 'pending']), new NewsEditor())->getData(true);
$this->assertEquals(422, $result['errcode']);
}
public function test_empty_batch_finishes_immediately(): void
{
$service = new NewsCollection(); $batch = $service->start(1);
@ -163,7 +199,7 @@ class CompanyNewsWorkflowTest extends TestCase
$this->provider([$this->response([$this->news('a', '2026-10-09 00:00:00'), $this->news('b'), $this->news('c', '2026-10-10 00:00:00')]), $this->response([$this->news('a')])]);
$service = new NewsCollection(); $service->start(1); $service->runOne(); $service->runOne(); $this->assertEquals(4, CompanyNews::count());
$controller = new CompanyNewsController();
$data = $controller->index(Request::create('/', 'GET', ['company_id' => 1, 'date_from' => '2026-10-09', 'date_to' => '2026-10-09']))->getData(true);
$data = $controller->index(Request::create('/', 'GET', ['review_status' => '', 'company_id' => 1, 'date_from' => '2026-10-09', 'date_to' => '2026-10-09']))->getData(true);
$this->assertEquals(2, $data['total']); $this->assertEquals([1, 1], array_column($data['data'], 'company_id'));
}
public function test_business_error_and_malformed_news_are_failures(): void
@ -184,7 +220,7 @@ class CompanyNewsWorkflowTest extends TestCase
'items' => $stale, 'review_status' => 'retained', 'is_visible' => true,
]), new NewsEditor())->getData(true);
$this->assertEquals(409, $response['errcode']);
$this->assertEquals(2, CompanyNews::where('review_status', 'pending')->count());
$this->assertEquals(2, CompanyNews::where('review_status', 'discarded')->count());
$this->assertEquals(0, DB::table('company_news_logs')->count());
$controller->reviewMany(Request::create('/', 'POST', [
'items' => $items, 'review_status' => 'retained', 'is_visible' => true,

@ -0,0 +1,61 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Http\Controllers\Admin\QccEnterpriseAccountController;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\Request;
class QccEnterpriseStudentFilterTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config(['database.default' => 'sqlite', 'database.connections.sqlite.database' => ':memory:', 'audit.enabled' => false]);
DB::purge('sqlite');
Schema::create('companies', function (Blueprint $t) { $t->id(); $t->string('company_name'); $t->string('credit_code'); $t->timestamps(); $t->softDeletes(); });
Schema::create('company_qcc_accounts', function (Blueprint $t) { $t->id(); $t->integer('company_id'); $t->string('status'); });
Schema::create('users', function (Blueprint $t) { $t->id(); $t->integer('company_id'); $t->string('name'); $t->string('username'); $t->string('no')->nullable(); $t->string('company_position')->nullable(); $t->integer('is_schoolmate'); $t->softDeletes(); });
Schema::create('courses', function (Blueprint $t) { $t->id(); $t->string('name'); $t->integer('type'); $t->date('start_date')->nullable(); $t->softDeletes(); });
Schema::create('course_signs', function (Blueprint $t) { $t->id(); $t->integer('course_id'); $t->integer('user_id'); $t->integer('status'); $t->softDeletes(); });
foreach ([1, 2, 3] as $id) {
DB::table('companies')->insert(['id' => $id, 'company_name' => '企业'.$id, 'credit_code' => 'credit'.$id]);
DB::table('company_qcc_accounts')->insert(['company_id' => $id, 'status' => 'occupied']);
}
DB::table('users')->insert([
['id' => 1, 'company_id' => 1, 'name' => '张三', 'username' => 'zhang', 'is_schoolmate' => 1],
['id' => 2, 'company_id' => 1, 'name' => '李四', 'username' => 'li', 'is_schoolmate' => 0],
['id' => 3, 'company_id' => 2, 'name' => '王五', 'username' => 'wang', 'is_schoolmate' => 0],
]);
DB::table('courses')->insert([['id' => 1, 'name' => '管理课程', 'type' => 1], ['id' => 2, 'name' => '金融课程', 'type' => 2]]);
DB::table('course_signs')->insert([
['user_id' => 1, 'course_id' => 1, 'status' => 1], ['user_id' => 2, 'course_id' => 2, 'status' => 1],
['user_id' => 1, 'course_id' => 2, 'status' => 0], ['user_id' => 3, 'course_id' => 2, 'status' => 0],
]);
}
private function query(array $params, string $method = 'candidates'): array
{
return (new QccEnterpriseAccountController())->$method(Request::create('/', 'GET', $params))->getData(true);
}
public function test_unfiltered_companies_remain_and_only_approved_students_are_shown(): void
{
$result = $this->query([]);
$this->assertEquals(3, $result['total']);
$company = collect($result['data'])->firstWhere('id', 1);
$this->assertCount(2, $company['users']);
$this->assertCount(1, $company['users'][0]['course_signs']);
$this->assertEquals('管理课程', $company['users'][0]['course_signs'][0]['course']['name']);
$this->assertEquals([], collect($result['data'])->firstWhere('id', 2)['users']);
}
public function test_combined_filters_match_same_student_and_same_approved_course(): void
{
foreach (['candidates', 'index'] as $method) {
$this->assertEquals(1, $this->query(['user_name' => '张', 'is_schoolmate' => '1', 'course_type_id' => 1, 'course_name' => '管理'], $method)['total']);
$this->assertEquals(0, $this->query(['user_name' => '张', 'course_name' => '金融'], $method)['total']);
$this->assertEquals(0, $this->query(['course_type_id' => 1, 'course_name' => '金融'], $method)['total']);
$this->assertEquals(1, $this->query(['is_schoolmate' => '0'], $method)['total']);
$this->assertEquals(0, $this->query(['user_name' => '王'], $method)['total']);
}
$this->assertCount(2, $this->query(['keyword' => 'credit1'], 'index')['data'][0]['company']['users']);
}
}
Loading…
Cancel
Save