'json', 'partners' => 'json']; protected $appends = ['is_yh_invested_text']; public function getIsYhInvestedTextAttribute() { if (empty($this->is_yh_invested)) { return ''; } return $this->is_yh_invested == 1 ? '被投企业' : ''; } public function users() { return $this->hasMany(User::class, 'company_id', 'id'); } /** * 限制只返回有关联学员且至少有一条审核通过的报名记录的公司 * 用于列表查询和统计查询 * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ public function scopeApprovedStudents($query) { return $query->whereHas('users', function ($q) { $q->whereHas('courseSigns', function ($signQuery) { $signQuery->where('status', 1); }); }); } /** * 根据区域名称筛选公司 * 支持区域名称映射和特殊逻辑(如"苏州市外") * @param \Illuminate\Database\Eloquent\Builder $query * @param string|array $companyArea 区域名称,多个用英文逗号分隔或传入数组 * @return \Illuminate\Database\Eloquent\Builder */ public function scopeFilterByArea($query, $companyArea) { if (empty($companyArea)) { return $query; } // 如果是字符串,转换为数组 if (is_string($companyArea)) { $company_area = array_filter(array_map('trim', explode(',', $companyArea))); } else { $company_area = array_filter(array_map('trim', (array) $companyArea)); } if (empty($company_area)) { return $query; } // 区域名称映射:搜索参数 -> 数据库值 $areaMapping = [ '高新区' => '虎丘', ]; // 检查是否包含"苏州市外" $hasSuzhouOut = in_array('苏州市外', $company_area); if ($hasSuzhouOut) { // 苏州市外:排除 company_area 参数接口返回的苏州市内选项,以及虎丘区 $excludeAreas = Parameter::where('number', 'company_area') ->with(['detail' => fn($q) => $q->orderBy('sort', 'asc')]) ->first(); $excludeList = []; if ($excludeAreas && $excludeAreas->detail) { foreach ($excludeAreas->detail as $d) { $v = trim((string) ($d->value ?? '')); if ($v !== '' && $v !== '苏州市外') { $excludeList[] = $v; // 高新区映射为虎丘(数据库可能存虎丘) if ($v === '高新区') { $excludeList[] = '虎丘'; } } } } $excludeList = array_unique(array_merge($excludeList, ['虎丘区'])); $query->whereNotNull('company_area')->where('company_area', '!=', ''); if (!empty($excludeList)) { foreach ($excludeList as $v) { $query->where('company_area', 'not like', '%' . $v . '%'); } } return $query; } else { // 将搜索参数转换为数据库值 $company_area = array_map(function ($v) use ($areaMapping) { return isset($areaMapping[$v]) ? $areaMapping[$v] : $v; }, $company_area); return $query->where(function ($q) use ($company_area) { foreach ($company_area as $v) { $q->orWhere('company_area', 'like', '%' . $v . '%'); } }); } } /** * 地址转经纬度 */ public static function addressTolocation($address) { $map = Config::getValueByKey('map_server'); $map = json_decode($map, true); $url = "https://restapi.amap.com/v3/geocode/geo"; $params = [ 'key' => $map['key'], 'address' => $address, ]; try { $result = httpCurl($url, 'GET', $params); $result = json_decode($result, true); if ($result['status'] == 1) { $location = $result['geocodes'][0]['location']; $location = explode(',', $location); return [ 'lng' => $location[0], 'lat' => $location[1], ]; } return [ 'lng' => null, 'lat' => null, ]; } catch (\Exception $e) { return [ 'lng' => null, 'lat' => null, ]; } } /** * 根据用户信息更新/同步公司信息 * @param User $user 用户对象 * @return array 返回结果 ['success' => bool, 'message' => string, 'company' => Company|null] */ public static function updateCompanyFromUser($user) { if (!$user || empty($user->company_name)) { return ['success' => false, 'message' => '用户或公司名称为空', 'company' => null]; } // 如果已经有有效的公司关联(company_id > 0),跳过 // 允许处理 company_id = -1(待更新)或 null(初始状态)的情况 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::getCompanyDetailByName($cleanedCompanyName); return self::syncUserCompanyByDetail($user, $result); } public static function updateCompanyFromCache($user) { if (!$user || empty($user->company_name)) { return ['success' => false, 'message' => '用户或公司名称为空', 'company' => null]; } 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' => $notFoundMessage, 'company' => null]; } // 如果$result['enterpriseName']存在数字,跳过 if (preg_match('/\d/', $result['enterpriseName'])) { $user->company_id = 0; $user->save(); return ['success' => false, 'message' => '公司名称包含数字,跳过', 'company' => null]; } if ($result['status'] == '未注册') { $user->company_id = 0; $user->save(); return ['success' => false, 'message' => '公司未注册,跳过', 'company' => null]; } $where = ['company_name' => $result['enterpriseName']]; $data = self::buildCompanyDataFromDetail($result, true); $company = Company::updateOrCreate($where, $data); // 更新用户关联 $user->company_id = $company->id; $user->save(); // 更新上市状态 self::updateMarketStatus($company->id); // 更新位置(经纬度) self::updateLocation($company->id); return ['success' => true, 'message' => '更新成功', 'company' => $company]; } /** * 直接同步公司信息(根据公司名称从接口获取最新信息更新) * @param Company $company 公司对象 * @return array 返回结果 ['success' => bool, 'message' => string, 'company' => Company|null] */ public static function syncCompanyInfo($company) { if (!$company || empty($company->company_name)) { return ['success' => false, 'message' => '公司或公司名称为空', 'company' => null]; } $cleanedCompanyName = self::normalizeCompanyName($company->company_name); if (empty($cleanedCompanyName)) { return ['success' => false, 'message' => '公司名称无效', 'company' => null]; } $result = self::fetchCompanyDetailFromApi($cleanedCompanyName); if (!$result) { return ['success' => false, 'message' => '公司不存在', 'company' => null]; } // 如果$result['enterpriseName']存在数字,跳过 if (preg_match('/\d/', $result['enterpriseName'])) { return ['success' => false, 'message' => '公司名称包含数字,跳过', 'company' => null]; } if ($result['status'] == '未注册') { return ['success' => false, 'message' => '公司未注册,跳过', 'company' => null]; } // 更新公司数据(不包含地址和经纬度) $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'], '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, ]; if ($includeAddress) { $data['company_address'] = $result['address'] ?? null; } return $data; } /** * 更新经纬度信息 * @param int $companyId 公司ID * @return bool */ public static function updateLocation($companyId) { $company = Company::find($companyId); if (!$company || empty($company->company_address) || $company->company_longitude) { return false; } $local = Company::addressTolocation($company->company_address); $company->company_longitude = $local['lng']; $company->company_latitude = $local['lat']; $company->save(); return true; } /** * 根据 company_tag 更新上市状态 * 判断是否包含上市代码标签,如 688001.SH、000001.SZ、830001.BJ 等 * @param int $companyId 公司ID * @return bool 是否更新成功 */ public static function updateMarketStatus($companyId) { $company = Company::find($companyId); if (!$company) { return false; } if (empty($company->company_tag)) { $newMarketStatus = 0; if ($company->company_market != $newMarketStatus) { $company->company_market = $newMarketStatus; $company->save(); return true; } 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'; $hasStockCode = preg_match($stockCodePattern, $company->company_tag); // 仅按股票代码判断上市状态,新三板标签不再计入上市公司 $newMarketStatus = $hasStockCode ? 1 : 0; // 只有状态变化才更新 if ($company->company_market != $newMarketStatus) { $company->company_market = $newMarketStatus; $company->save(); return true; } return false; } /** * 验证公司名称是否包含特殊符号 * @param string $companyName 公司名称 * @return array 返回结果 ['valid' => bool, 'message' => string] */ public static function validateCompanyName($companyName) { if (empty($companyName)) { return ['valid' => true, 'message' => '']; } // 定义不允许的特殊符号(包含中英文标点符号,键盘上能打出来的所有标点符号) $forbiddenChars = [ // 英文标点符号 '/', '\\', '.', ',', ';', ':', "'", '"', '?', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '[', ']', '{', '}', '|', '`', '~', '-', '_', '+', '=', '<', '>', // 中文标点符号 '。', ',', '、', ';', ':', '?', '!', '…', '—', '·', '~', '¥', '(', ')', '【', '】', '《', '》', '〈', '〉', '「', '」', '『', '』', '〔', '〕', ]; // 添加中文引号字符(使用十六进制编码避免语法错误) $chineseQuotes = ["\xE2\x80\x9C", "\xE2\x80\x9D", "\xE2\x80\x98", "\xE2\x80\x99"]; // " " ' ' $forbiddenChars = array_merge($forbiddenChars, $chineseQuotes); foreach ($forbiddenChars as $char) { if (strpos($companyName, $char) !== false) { return ['valid' => false, 'message' => '公司名称不能包含特殊符号']; } } return ['valid' => true, 'message' => '']; } }