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.

549 lines
18 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Models;
use App\Repositories\YuanheRepository;
use Illuminate\Support\Arr;
class Company extends SoftDeletesModel
{
protected $casts = ['project_users' => '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' => ''];
}
}