diff --git a/app/Console/Commands/RefreshCompanyMarketStatus.php b/app/Console/Commands/RefreshCompanyMarketStatus.php new file mode 100644 index 0000000..4f937d8 --- /dev/null +++ b/app/Console/Commands/RefreshCompanyMarketStatus.php @@ -0,0 +1,91 @@ +option('company_id'); + $chunk = (int) $this->option('chunk'); + $chunk = $chunk > 0 ? $chunk : 500; + + $query = Company::query()->orderBy('id'); + + if (!empty($companyId)) { + $query->where('id', $companyId); + } + + $total = (clone $query)->count(); + if ($total == 0) { + return $this->info('没有需要处理的公司'); + } + + $this->info("开始重算 company_market,共 {$total} 家公司"); + $bar = $this->output->createProgressBar($total); + $bar->start(); + + $updatedCount = 0; + $unchangedCount = 0; + $failCount = 0; + + $query->chunkById($chunk, function ($companies) use (&$updatedCount, &$unchangedCount, &$failCount, $bar) { + foreach ($companies as $company) { + try { + $updated = Company::updateMarketStatus($company->id); + + if ($updated) { + $updatedCount++; + $bar->setMessage($company->company_name . ' 已更新', 'status'); + } else { + $unchangedCount++; + $bar->setMessage($company->company_name . ' 无变化', 'status'); + } + } catch (\Throwable $e) { + $failCount++; + $bar->setMessage($company->company_name . ' 失败: ' . $e->getMessage(), 'status'); + } + + $bar->advance(); + } + }); + + $bar->finish(); + $this->newLine(); + $this->info("处理完成:更新 {$updatedCount} 家,未变化 {$unchangedCount} 家,失败 {$failCount} 家"); + + return $this->info('company_market 全量重算完成'); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index a3442af..ec3984a 100755 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -34,7 +34,7 @@ class Kernel extends ConsoleKernel // 更新课程校友资格 $schedule->command('auto_schoolmate')->everyThirtyMinutes(); // 更新公司信息 - $schedule->command('update_company')->everyFiveMinutes(); + // $schedule->command('update_company')->everyFiveMinutes(); // 全量同步公司信息(每天凌晨执行,不同步经纬度和地址) // $schedule->command('sync:company')->dailyAt('02:00'); // 同步老师课程方向 diff --git a/app/Http/Controllers/Mobile/OtherController.php b/app/Http/Controllers/Mobile/OtherController.php index 825bab7..b7a0f04 100755 --- a/app/Http/Controllers/Mobile/OtherController.php +++ b/app/Http/Controllers/Mobile/OtherController.php @@ -10,6 +10,7 @@ use App\Models\AppointmentConfig; use App\Models\AppointmentType; use App\Models\Banner; use App\Models\Company; +use App\Models\CompanyDetailCache; use App\Models\Config; use App\Models\CourseType; use App\Repositories\YuanheRepository; @@ -132,11 +133,36 @@ class OtherController extends CommonController if ($validator->fails()) { return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]); } + $normalizedCompanyName = Company::normalizeCompanyName($all['company_name']); + $cache = CompanyDetailCache::query() + ->where('normalized_company_name', $normalizedCompanyName) + ->orWhere('normalized_enterprise_name', $normalizedCompanyName) + ->orderByDesc('fetched_at') + ->first(); + + if ($cache && !empty($cache->payload)) { + $cache->last_matched_at = now(); + $cache->save(); + return $this->success($cache->payload); + } + $YuanheRepository = new YuanheRepository(); $result = $YuanheRepository->companyInfo(['enterpriseName' => $all['company_name']]); if (empty($result)) { return $this->fail([ResponseCode::ERROR_PARAMETER, '无数据']); } + CompanyDetailCache::updateOrCreate( + ['normalized_company_name' => $normalizedCompanyName], + [ + 'query_company_name' => trim($all['company_name']), + 'enterprise_name' => $result['enterpriseName'] ?? null, + 'normalized_enterprise_name' => Company::normalizeCompanyName($result['enterpriseName'] ?? null), + 'credit_code' => $result['creditCode'] ?? null, + 'enterprise_id' => $result['enterpriseId'] ?? null, + 'payload' => $result, + 'fetched_at' => now(), + ] + ); return $this->success($result); } diff --git a/app/Http/Controllers/Mobile/UserController.php b/app/Http/Controllers/Mobile/UserController.php index 75844bf..dbf3b0d 100755 --- a/app/Http/Controllers/Mobile/UserController.php +++ b/app/Http/Controllers/Mobile/UserController.php @@ -167,6 +167,7 @@ class UserController extends CommonController { $all = \request()->except(['id', 'mobile', 'openid']); $model = User::find($this->getUserId()); + $oldCompanyName = $model->company_name; if (isset($all['password'])) { // 判断旧密码是否正确 if (!Hash::check($all['old_password'], $model->password)) { @@ -192,10 +193,11 @@ class UserController extends CommonController $model->fill($all); $model->save(); // 如果有公司信息,就更新一下公司 - if (isset($all['company_name']) && !empty($all['company_name']) && $model->company_name != $all['company_name']) { - // 设置待更新标记,由定时任务处理 - $model->company_id = -1; + if (isset($all['company_name']) && !empty($all['company_name']) && $oldCompanyName != $all['company_name']) { + $model->company_id = null; $model->save(); + + Company::updateCompanyFromCache($model); } // 判断下,如果用户新加入车牌号,并且有未开始或者进行中的预约,则直接预约车牌号 $appointmentModel = Appointment::where('user_id', $this->getUserId()) diff --git a/app/Http/functions.php b/app/Http/functions.php index 5f8d727..e5e3d3c 100755 --- a/app/Http/functions.php +++ b/app/Http/functions.php @@ -502,7 +502,7 @@ function friendly_date2($sTime, $cTime = false, $type = 'mohu', $show_after_or_b // 获取当前域名 function getDomain() { - return get_http_type() . $_SERVER['HTTP_HOST']; + return config('app.url'); } /** diff --git a/app/Models/Company.php b/app/Models/Company.php index 778e43a..fdb679b 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Repositories\YuanheRepository; +use Illuminate\Support\Arr; class Company extends SoftDeletesModel { @@ -73,7 +74,7 @@ class Company extends SoftDeletesModel if ($hasSuzhouOut) { // 苏州市外:排除 company_area 参数接口返回的苏州市内选项,以及虎丘区 $excludeAreas = Parameter::where('number', 'company_area') - ->with(['detail' => fn ($q) => $q->orderBy('sort', 'asc')]) + ->with(['detail' => fn($q) => $q->orderBy('sort', 'asc')]) ->first(); $excludeList = []; if ($excludeAreas && $excludeAreas->detail) { @@ -163,30 +164,46 @@ class Company extends SoftDeletesModel return ['success' => false, 'message' => '用户已有公司关联', 'company' => null]; } - // 清理公司名称:去除首尾空格、换行符、制表符等异常字符 - $cleanedCompanyName = trim($user->company_name); - // 去除换行符、回车符、制表符等空白字符 - $cleanedCompanyName = preg_replace('/[\r\n\t]+/', '', $cleanedCompanyName); - // 将多个连续空格替换为单个空格 - $cleanedCompanyName = preg_replace('/\s+/', ' ', $cleanedCompanyName); - // 再次去除首尾空格 - $cleanedCompanyName = trim($cleanedCompanyName); + $cleanedCompanyName = self::normalizeCompanyName($user->company_name); // 如果清理后为空,返回错误 if (empty($cleanedCompanyName)) { return ['success' => false, 'message' => '公司名称无效', 'company' => null]; } - $YuanheRepository = new YuanheRepository(); + $result = self::getCompanyDetailByName($cleanedCompanyName); + + return self::syncUserCompanyByDetail($user, $result); + } + + public static function updateCompanyFromCache($user) + { + if (!$user || empty($user->company_name)) { + return ['success' => false, 'message' => '用户或公司名称为空', 'company' => null]; + } - // 获取公司详细信息,使用清理后的公司名称 - $result = $YuanheRepository->companyInfo(['enterpriseName' => $cleanedCompanyName]); + if ($user->company_id && $user->company_id > 0) { + return ['success' => false, 'message' => '用户已有公司关联', 'company' => null]; + } + + $cleanedCompanyName = self::normalizeCompanyName($user->company_name); + if (empty($cleanedCompanyName)) { + return ['success' => false, 'message' => '公司名称无效', 'company' => null]; + } + + $result = self::getCachedCompanyDetailByName($cleanedCompanyName); + + return self::syncUserCompanyByDetail($user, $result, '公司缓存不存在'); + } + + protected static function syncUserCompanyByDetail($user, $result, $notFoundMessage = '公司不存在') + { if (!$result) { // 标识一下未匹配到公司,后续可以根据这个字段筛选出未匹配到公司的用户 $user->company_id = 0; $user->save(); - return ['success' => false, 'message' => '公司不存在', 'company' => null]; + return ['success' => false, 'message' => $notFoundMessage, 'company' => null]; } // 如果$result['enterpriseName']存在数字,跳过 @@ -203,40 +220,7 @@ class Company extends SoftDeletesModel } $where = ['company_name' => $result['enterpriseName']]; - $data = [ - 'business_scope' => $result['businessScope'], - 'company_city' => $result['city'], - 'contact_mail' => $result['contactMail'], - 'contact_phone' => $result['contactPhone'], - 'company_area' => $result['country'], - 'credit_code' => $result['creditCode'], - 'enterprise_id' => $result['enterpriseId'], - 'company_name' => $result['enterpriseName'], - 'is_abroad' => $result['isAbroad'], - 'company_market' => $result['isOnStock'], - 'is_yh_invested' => $result['isYhInvested'], - 'logo' => $result['logo'], - 'company_legal_representative' => $result['operName'], - 'company_province' => $result['province'], - 'company_industry' => combineKeyValue($result['qccIndustry']), - 'regist_amount' => $result['registAmount'], - 'regist_capi_type' => $result['registCapiType'], - 'company_date' => $result['startDate'], - 'status' => $result['status'], - 'stock_date' => $result['stockDate'], - 'currency_type' => $result['currencyType'], - 'stock_number' => $result['stockNumber'], - 'stock_type' => $result['stockType'], - 'company_tag' => implode(',', $result['tagList']), - // 更新日期 - 'update_date' => $result['updatedDate'] ?? null, - // 管理平台 - 'project_users' => $result['projectUsers'] ?? null, - // 股东信息 - 'partners' => $result['partners'] ?? null, - // 更新地址 - 'company_address' => $result['address'], - ]; + $data = self::buildCompanyDataFromDetail($result, true); $company = Company::updateOrCreate($where, $data); @@ -264,20 +248,13 @@ class Company extends SoftDeletesModel return ['success' => false, 'message' => '公司或公司名称为空', 'company' => null]; } - // 清理公司名称 - $cleanedCompanyName = trim($company->company_name); - $cleanedCompanyName = preg_replace('/[\r\n\t]+/', '', $cleanedCompanyName); - $cleanedCompanyName = preg_replace('/\s+/', ' ', $cleanedCompanyName); - $cleanedCompanyName = trim($cleanedCompanyName); + $cleanedCompanyName = self::normalizeCompanyName($company->company_name); if (empty($cleanedCompanyName)) { return ['success' => false, 'message' => '公司名称无效', 'company' => null]; } - $YuanheRepository = new YuanheRepository(); - - // 获取公司详细信息 - $result = $YuanheRepository->companyInfo(['enterpriseName' => $cleanedCompanyName]); + $result = self::fetchCompanyDetailFromApi($cleanedCompanyName); if (!$result) { return ['success' => false, 'message' => '公司不存在', 'company' => null]; @@ -293,6 +270,95 @@ class Company extends SoftDeletesModel } // 更新公司数据(不包含地址和经纬度) + $data = self::buildCompanyDataFromDetail($result, false); + + $company->fill($data); + $company->save(); + + // 更新上市状态 + self::updateMarketStatus($company->id); + + return ['success' => true, 'message' => '更新成功', 'company' => $company]; + } + + public static function normalizeCompanyName($companyName) + { + if ($companyName === null) { + return null; + } + + $companyName = trim($companyName); + $companyName = preg_replace('/[\r\n\t]+/', '', $companyName); + $companyName = preg_replace('/\s+/', ' ', $companyName); + + return trim($companyName); + } + + protected static function getCompanyDetailByName($companyName) + { + $normalizedCompanyName = self::normalizeCompanyName($companyName); + if (empty($normalizedCompanyName)) { + return false; + } + + $cachePayload = self::getCachedCompanyDetailByName($normalizedCompanyName); + if ($cachePayload) { + return $cachePayload; + } + + return self::fetchCompanyDetailFromApi($normalizedCompanyName); + } + + protected static function getCachedCompanyDetailByName($companyName) + { + $normalizedCompanyName = self::normalizeCompanyName($companyName); + if (empty($normalizedCompanyName)) { + return false; + } + + $cache = CompanyDetailCache::query() + ->where('normalized_company_name', $normalizedCompanyName) + ->orWhere('normalized_enterprise_name', $normalizedCompanyName) + ->orderByDesc('fetched_at') + ->first(); + + if (!$cache || empty($cache->payload)) { + return false; + } + + $cache->last_matched_at = now(); + $cache->save(); + + return $cache->payload; + } + + protected static function fetchCompanyDetailFromApi($companyName) + { + $YuanheRepository = new YuanheRepository(); + $result = $YuanheRepository->companyInfo(['enterpriseName' => $companyName]); + + if (!$result) { + return false; + } + + CompanyDetailCache::updateOrCreate( + ['normalized_company_name' => self::normalizeCompanyName($companyName)], + [ + 'query_company_name' => $companyName, + 'enterprise_name' => Arr::get($result, 'enterpriseName'), + 'normalized_enterprise_name' => self::normalizeCompanyName(Arr::get($result, 'enterpriseName')), + 'credit_code' => Arr::get($result, 'creditCode'), + 'enterprise_id' => Arr::get($result, 'enterpriseId'), + 'payload' => $result, + 'fetched_at' => now(), + ] + ); + + return $result; + } + + protected static function buildCompanyDataFromDetail(array $result, $includeAddress = false) + { $data = [ 'business_scope' => $result['businessScope'], 'company_city' => $result['city'], @@ -317,19 +383,17 @@ class Company extends SoftDeletesModel 'currency_type' => $result['currencyType'], 'stock_number' => $result['stockNumber'], 'stock_type' => $result['stockType'], - 'company_tag' => implode(',', $result['tagList']), + 'company_tag' => implode(',', $result['tagList'] ?? []), 'update_date' => $result['updatedDate'] ?? null, 'project_users' => $result['projectUsers'] ?? null, 'partners' => $result['partners'] ?? null, ]; - $company->fill($data); - $company->save(); - - // 更新上市状态 - self::updateMarketStatus($company->id); + if ($includeAddress) { + $data['company_address'] = $result['address'] ?? null; + } - return ['success' => true, 'message' => '更新成功', 'company' => $company]; + return $data; } /** @@ -360,36 +424,29 @@ class Company extends SoftDeletesModel public static function updateMarketStatus($companyId) { $company = Company::find($companyId); - if (!$company || empty($company->company_tag)) { + if (!$company) { return false; } - // 上市代码正则:匹配全球各地上市公司股票代码后缀 - $stockCodePattern = '/\.(SWR|SW|WR|SS|RS|SB|PK|TO|AX|WS|PR|DB|UN|RT|WT|SH|SZ|BJ|TW|HK|SG|US|DE|FR|JP|KR|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|U|V|W|X|Y|Z)(?![A-Za-z0-9])/i'; + if (empty($company->company_tag)) { + $newMarketStatus = 0; - $hasStockCode = preg_match($stockCodePattern, $company->company_tag); + if ($company->company_market != $newMarketStatus) { + $company->company_market = $newMarketStatus; + $company->save(); + return true; + } - // 不属于新三板上市公司的关键字(需要排除) - $excludeXinsanbanKeywords = [ - '新三板摘牌', - '新三板挂牌审核', - '新三板终止', - '新三板退市', - '新三板撤销', - '新三板注销', - '新三板中止', - ]; + return false; + } - // 检查是否包含排除关键字 - $hasExcludeKeyword = array_reduce($excludeXinsanbanKeywords, function ($carry, $keyword) use ($company) { - return $carry || strpos($company->company_tag, $keyword) !== false; - }, false); + // 上市代码正则:匹配全球各地上市公司股票代码后缀 + $stockCodePattern = '/\.(SWR|SW|WR|SS|RS|SB|PK|TO|AX|WS|PR|DB|UN|RT|WT|SH|SZ|BJ|TW|HK|SG|US|DE|FR|JP|KR|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|U|V|W|X|Y|Z)(?![A-Za-z0-9])/i'; - // 检查是否包含"新三板",且不包含排除关键字 - $hasXinsanban = !$hasExcludeKeyword && strpos($company->company_tag, '新三板') !== false; + $hasStockCode = preg_match($stockCodePattern, $company->company_tag); - // 如果匹配到股票代码或包含"新三板"(且非排除关键字),则标记为上市 - $newMarketStatus = ($hasStockCode || $hasXinsanban) ? 1 : 0; + // 仅按股票代码判断上市状态,新三板标签不再计入上市公司 + $newMarketStatus = $hasStockCode ? 1 : 0; // 只有状态变化才更新 if ($company->company_market != $newMarketStatus) { diff --git a/app/Models/CompanyDetailCache.php b/app/Models/CompanyDetailCache.php new file mode 100644 index 0000000..0876d5a --- /dev/null +++ b/app/Models/CompanyDetailCache.php @@ -0,0 +1,13 @@ + 'array', + 'fetched_at' => 'datetime', + 'last_matched_at' => 'datetime', + ]; +} diff --git a/database/migrations/2026_03_26_120000_create_company_detail_caches_table.php b/database/migrations/2026_03_26_120000_create_company_detail_caches_table.php new file mode 100644 index 0000000..68c4299 --- /dev/null +++ b/database/migrations/2026_03_26_120000_create_company_detail_caches_table.php @@ -0,0 +1,41 @@ +id(); + $table->string('query_company_name')->comment('前端查询公司名称'); + $table->string('normalized_company_name')->unique()->comment('标准化查询公司名称'); + $table->string('enterprise_name')->nullable()->comment('第三方返回企业名称'); + $table->string('normalized_enterprise_name')->nullable()->index()->comment('标准化企业名称'); + $table->string('credit_code')->nullable()->index()->comment('统一社会信用代码'); + $table->string('enterprise_id')->nullable()->index()->comment('企业ID'); + $table->json('payload')->comment('第三方公司详情暂存数据'); + $table->dateTime('fetched_at')->nullable()->comment('抓取时间'); + $table->dateTime('last_matched_at')->nullable()->comment('最近匹配使用时间'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('company_detail_caches'); + } +};