0806
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
/**
|
||||
* 国际电话国家区号表(区号 => 中文国籍/地区名)
|
||||
* 用于海外手机号码归属地识别(对应 DeepSeek 方案一中 phonenumbers 库的国家识别能力)。
|
||||
* 覆盖常用国家/地区;未收录区号将返回"未知"。
|
||||
*/
|
||||
return [
|
||||
// 1 区:北美
|
||||
'1' => '美国/加拿大',
|
||||
// 7 区:俄罗斯/哈萨克斯坦
|
||||
'7' => '俄罗斯',
|
||||
// 20 区
|
||||
'20' => '埃及',
|
||||
// 27 区:南非
|
||||
'27' => '南非',
|
||||
// 30-39 区:欧洲
|
||||
'30' => '希腊',
|
||||
'31' => '荷兰',
|
||||
'32' => '比利时',
|
||||
'33' => '法国',
|
||||
'34' => '西班牙',
|
||||
'36' => '匈牙利',
|
||||
'39' => '意大利',
|
||||
// 40-49 区
|
||||
'40' => '罗马尼亚',
|
||||
'41' => '瑞士',
|
||||
'43' => '奥地利',
|
||||
'44' => '英国',
|
||||
'45' => '丹麦',
|
||||
'46' => '瑞典',
|
||||
'47' => '挪威',
|
||||
'48' => '波兰',
|
||||
'49' => '德国',
|
||||
// 50-59 区
|
||||
'51' => '秘鲁',
|
||||
'52' => '墨西哥',
|
||||
'53' => '古巴',
|
||||
'54' => '阿根廷',
|
||||
'55' => '巴西',
|
||||
'56' => '智利',
|
||||
'57' => '哥伦比亚',
|
||||
'58' => '委内瑞拉',
|
||||
// 60-69 区
|
||||
'60' => '马来西亚',
|
||||
'61' => '澳大利亚',
|
||||
'62' => '印度尼西亚',
|
||||
'63' => '菲律宾',
|
||||
'64' => '新西兰',
|
||||
'65' => '新加坡',
|
||||
'66' => '泰国',
|
||||
// 7x 区:俄罗斯等(已在上方)
|
||||
// 80-89 区
|
||||
'81' => '日本',
|
||||
'82' => '韩国',
|
||||
'84' => '越南',
|
||||
'86' => '中国',
|
||||
// 90-99 区
|
||||
'90' => '土耳其',
|
||||
'91' => '印度',
|
||||
'92' => '巴基斯坦',
|
||||
'93' => '阿富汗',
|
||||
'94' => '斯里兰卡',
|
||||
'95' => '缅甸',
|
||||
'96' => '约旦',
|
||||
'97' => '叙利亚',
|
||||
'98' => '伊朗',
|
||||
// 其他常见
|
||||
'211' => '南苏丹',
|
||||
'212' => '摩洛哥',
|
||||
'213' => '阿尔及利亚',
|
||||
'216' => '突尼斯',
|
||||
'218' => '利比亚',
|
||||
'220' => '冈比亚',
|
||||
'221' => '塞内加尔',
|
||||
'222' => '毛里塔尼亚',
|
||||
'223' => '马里',
|
||||
'224' => '几内亚',
|
||||
'225' => '科特迪瓦',
|
||||
'226' => '布基纳法索',
|
||||
'227' => '尼日尔',
|
||||
'228' => '多哥',
|
||||
'229' => '贝宁',
|
||||
'230' => '毛里求斯',
|
||||
'231' => '利比里亚',
|
||||
'232' => '塞拉利昂',
|
||||
'233' => '加纳',
|
||||
'234' => '尼日利亚',
|
||||
'235' => '乍得',
|
||||
'236' => '中非',
|
||||
'237' => '喀麦隆',
|
||||
'238' => '佛得角',
|
||||
'239' => '圣多美和普林西比',
|
||||
'240' => '赤道几内亚',
|
||||
'241' => '加蓬',
|
||||
'242' => '刚果(布)',
|
||||
'243' => '刚果(金)',
|
||||
'244' => '安哥拉',
|
||||
'245' => '几内亚比绍',
|
||||
'246' => '迪戈加西亚',
|
||||
'248' => '塞舌尔',
|
||||
'249' => '苏丹',
|
||||
'250' => '卢旺达',
|
||||
'251' => '埃塞俄比亚',
|
||||
'252' => '索马里',
|
||||
'253' => '吉布提',
|
||||
'254' => '肯尼亚',
|
||||
'255' => '坦桑尼亚',
|
||||
'256' => '乌干达',
|
||||
'257' => '布隆迪',
|
||||
'258' => '莫桑比克',
|
||||
'260' => '赞比亚',
|
||||
'261' => '马达加斯加',
|
||||
'262' => '留尼汪/马约特',
|
||||
'263' => '津巴布韦',
|
||||
'264' => '纳米比亚',
|
||||
'265' => '马拉维',
|
||||
'266' => '莱索托',
|
||||
'267' => '博茨瓦纳',
|
||||
'268' => '斯威士兰',
|
||||
'269' => '科摩罗',
|
||||
'290' => '圣赫勒拿',
|
||||
'291' => '厄立特里亚',
|
||||
'297' => '阿鲁巴',
|
||||
'298' => '法罗群岛',
|
||||
'299' => '格陵兰',
|
||||
'350' => '直布罗陀',
|
||||
'351' => '葡萄牙',
|
||||
'352' => '卢森堡',
|
||||
'353' => '爱尔兰',
|
||||
'354' => '冰岛',
|
||||
'355' => '阿尔巴尼亚',
|
||||
'356' => '马耳他',
|
||||
'357' => '塞浦路斯',
|
||||
'358' => '芬兰',
|
||||
'359' => '保加利亚',
|
||||
'370' => '立陶宛',
|
||||
'371' => '拉脱维亚',
|
||||
'372' => '爱沙尼亚',
|
||||
'373' => '摩尔多瓦',
|
||||
'374' => '亚美尼亚',
|
||||
'375' => '白俄罗斯',
|
||||
'376' => '安道尔',
|
||||
'377' => '摩纳哥',
|
||||
'378' => '圣马力诺',
|
||||
'380' => '乌克兰',
|
||||
'381' => '塞尔维亚',
|
||||
'382' => '黑山',
|
||||
'383' => '科索沃',
|
||||
'385' => '克罗地亚',
|
||||
'386' => '斯洛文尼亚',
|
||||
'387' => '波黑',
|
||||
'389' => '北马其顿',
|
||||
'420' => '捷克',
|
||||
'421' => '斯洛伐克',
|
||||
'423' => '列支敦士登',
|
||||
'500' => '福克兰群岛',
|
||||
'501' => '伯利兹',
|
||||
'502' => '危地马拉',
|
||||
'503' => '萨尔瓦多',
|
||||
'504' => '洪都拉斯',
|
||||
'505' => '尼加拉瓜',
|
||||
'506' => '哥斯达黎加',
|
||||
'507' => '巴拿马',
|
||||
'508' => '圣皮埃尔和密克隆',
|
||||
'509' => '海地',
|
||||
'590' => '瓜德罗普',
|
||||
'591' => '玻利维亚',
|
||||
'592' => '圭亚那',
|
||||
'593' => '厄瓜多尔',
|
||||
'594' => '法属圭亚那',
|
||||
'595' => '巴拉圭',
|
||||
'596' => '马提尼克',
|
||||
'597' => '苏里南',
|
||||
'598' => '乌拉圭',
|
||||
'599' => '加勒比荷兰',
|
||||
'670' => '东帝汶',
|
||||
'672' => '南极领地',
|
||||
'673' => '文莱',
|
||||
'674' => '瑙鲁',
|
||||
'675' => '巴布亚新几内亚',
|
||||
'676' => '汤加',
|
||||
'677' => '所罗门群岛',
|
||||
'678' => '瓦努阿图',
|
||||
'679' => '斐济',
|
||||
'680' => '帕劳',
|
||||
'681' => '瓦利斯和富图纳',
|
||||
'682' => '库克群岛',
|
||||
'683' => '纽埃',
|
||||
'685' => '萨摩亚',
|
||||
'686' => '基里巴斯',
|
||||
'687' => '新喀里多尼亚',
|
||||
'688' => '图瓦卢',
|
||||
'689' => '法属波利尼西亚',
|
||||
'690' => '托克劳',
|
||||
'691' => '密克罗尼西亚',
|
||||
'692' => '马绍尔群岛',
|
||||
'850' => '朝鲜',
|
||||
'852' => '中国香港',
|
||||
'853' => '中国澳门',
|
||||
'855' => '柬埔寨',
|
||||
'856' => '老挝',
|
||||
'880' => '孟加拉国',
|
||||
'886' => '中国台湾',
|
||||
'960' => '马尔代夫',
|
||||
'961' => '黎巴嫩',
|
||||
'962' => '约旦',
|
||||
'963' => '叙利亚',
|
||||
'964' => '伊拉克',
|
||||
'965' => '科威特',
|
||||
'966' => '沙特阿拉伯',
|
||||
'967' => '也门',
|
||||
'968' => '阿曼',
|
||||
'970' => '巴勒斯坦',
|
||||
'971' => '阿联酋',
|
||||
'972' => '以色列',
|
||||
'973' => '巴林',
|
||||
'974' => '卡塔尔',
|
||||
'975' => '不丹',
|
||||
'976' => '蒙古',
|
||||
'977' => '尼泊尔',
|
||||
'992' => '塔吉克斯坦',
|
||||
'993' => '土库曼斯坦',
|
||||
'994' => '阿塞拜疆',
|
||||
'995' => '格鲁吉亚',
|
||||
'996' => '吉尔吉斯斯坦',
|
||||
'998' => '乌兹别克斯坦',
|
||||
];
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* 数据字典接口 GET /api/common/dicts.php
|
||||
* 参数:type=country|area|industry|certificate|source_channel|all(默认 all)
|
||||
* 返回各字典表启用中的数据,供前端下拉/联动使用。
|
||||
* 数据来源:官方 Excel 生成的种子数据(见 sql/dict_seed.sql),不依赖任何第三方接口。
|
||||
*/
|
||||
require_once __DIR__ . '/db.php';
|
||||
require_once __DIR__ . '/response.php';
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
requireLogin();
|
||||
|
||||
$type = trim($_REQUEST['type'] ?? 'all');
|
||||
$pdo = DB::getInstance()->getPdo();
|
||||
|
||||
$data = [];
|
||||
|
||||
if ($type === 'all' || $type === 'country') {
|
||||
$data['countries'] = $pdo->query(
|
||||
"SELECT id, code, name FROM date_dict_country WHERE is_active = 1 ORDER BY sort_order, id"
|
||||
)->fetchAll();
|
||||
}
|
||||
if ($type === 'all' || $type === 'area') {
|
||||
$data['areas'] = $pdo->query(
|
||||
"SELECT id, code, name, parent_code FROM date_dict_area WHERE is_active = 1 ORDER BY sort_order, id"
|
||||
)->fetchAll();
|
||||
}
|
||||
if ($type === 'all' || $type === 'industry') {
|
||||
$data['industries'] = $pdo->query(
|
||||
"SELECT id, code, name, parent_code FROM date_dict_industry WHERE is_active = 1 ORDER BY sort_order, id"
|
||||
)->fetchAll();
|
||||
}
|
||||
if ($type === 'all' || $type === 'certificate') {
|
||||
$data['certificates'] = $pdo->query(
|
||||
"SELECT id, code, name FROM date_dict_certificate WHERE is_active = 1 ORDER BY sort_order, id"
|
||||
)->fetchAll();
|
||||
}
|
||||
if ($type === 'all' || $type === 'source_channel') {
|
||||
$data['source_channels'] = $pdo->query(
|
||||
"SELECT id, name FROM source_channels WHERE is_active = 1 ORDER BY sort_order, id"
|
||||
)->fetchAll();
|
||||
}
|
||||
|
||||
Response::success($data);
|
||||
@@ -14,6 +14,7 @@ const COMPANY_FIELDS = [
|
||||
'industry' => 's', 'industry_subdivision' => 's',
|
||||
'latest_employee_count' => 'i', 'latest_annual_revenue' => 's',
|
||||
'is_listed' => 'i', 'stock_code' => 's', 'website' => 's',
|
||||
'source_channel' => 's', 'source_detail' => 's',
|
||||
];
|
||||
|
||||
/** 人员表可写入字段白名单 */
|
||||
@@ -21,12 +22,13 @@ const PERSON_FIELDS = [
|
||||
'union_id' => 's', 'full_name' => 's', 'gender' => 's', 'nationality' => 's',
|
||||
'id_type' => 's', 'id_number' => 's', 'education' => 's', 'graduated_from' => 's',
|
||||
'hometown' => 's', 'work_location' => 's',
|
||||
'source_channel' => 's', 'source_detail' => 's',
|
||||
];
|
||||
|
||||
/** 媒体(社交账号)表可写入字段白名单 */
|
||||
const MEDIA_FIELDS = [
|
||||
'owner_type' => 's', 'owner_id' => 'i', 'platform' => 's', 'account_id' => 's',
|
||||
'profile_url' => 's', 'remark' => 's', 'is_primary' => 'i',
|
||||
'profile_url' => 's', 'remark' => 's', 'is_primary' => 'i', 'is_defult' => 'i',
|
||||
];
|
||||
|
||||
/** 媒体商业属性表可写入字段白名单 */
|
||||
@@ -173,7 +175,7 @@ function saveCompanyCertifications($pdo, $companyId, $certifications)
|
||||
return;
|
||||
}
|
||||
|
||||
$valid = $pdo->prepare("SELECT id FROM certification_types WHERE id = ?");
|
||||
$valid = $pdo->prepare("SELECT id FROM date_dict_certificate WHERE id = ?");
|
||||
$ins = $pdo->prepare(
|
||||
"INSERT INTO company_certifications
|
||||
(company_id, certification_type_id, certificate_number, issue_date, expiry_date)
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
/**
|
||||
* 手机号码归属地识别接口 GET /api/common/phone_geo.php?phone=13812345678
|
||||
* 需求:录入手机号码时自动识别归属地
|
||||
* - 国内号码:识别"国家-省-市"(如 中国-广东省-深圳市)
|
||||
* - 港澳台: 标注为"中国香港"/"中国澳门"/"中国台湾"
|
||||
* - 海外号码:识别"国籍"(如 美国)
|
||||
* 实现方案(参考 DeepSeek 方案一:phonenumbers + phone 双库组合):
|
||||
* - 国内省市:使用 phone.dat 数据库(Google libphonenumber 同源号段数据,见 data/phone.dat)
|
||||
* - 国际号码:内置国家区号表识别国籍
|
||||
* 返回:{ country, province, city, label, found }
|
||||
* label 示例:"中国-广东省-深圳市" / "中国香港" / "美国"
|
||||
*/
|
||||
require_once __DIR__ . '/db.php';
|
||||
require_once __DIR__ . '/response.php';
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
/**
|
||||
* 接口入口:仅当作为 HTTP 接口直接访问时执行(便于函数被 require 复用/测试)
|
||||
*/
|
||||
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) {
|
||||
requireLogin();
|
||||
|
||||
$phone = trim($_REQUEST['phone'] ?? '');
|
||||
if ($phone === '') {
|
||||
Response::error('参数错误:缺少 phone', 400);
|
||||
}
|
||||
|
||||
Response::success(detectPhoneLocation($phone));
|
||||
}
|
||||
|
||||
/* ==================== 归属地识别核心 ==================== */
|
||||
|
||||
/**
|
||||
* 解析号码归属地
|
||||
* @param string $phone 原始号码(可含 + / 空格 / 短横线)
|
||||
* @return array
|
||||
*/
|
||||
function detectPhoneLocation($phone)
|
||||
{
|
||||
$digits = preg_replace('/[^\d]/', '', $phone);
|
||||
$hasPlus = strpos($phone, '+') === 0;
|
||||
|
||||
// 处理国际前缀:+86 / 0086 / 86 开头统一识别
|
||||
// 0086 开头 → 转为 86 开头
|
||||
if (strpos($digits, '00') === 0) {
|
||||
$digits = substr($digits, 2);
|
||||
$hasPlus = true;
|
||||
}
|
||||
|
||||
// 1) 港澳台特殊标注
|
||||
$special = specialRegion($digits);
|
||||
if ($special !== null) {
|
||||
return ['country' => $special, 'province' => '', 'city' => '', 'label' => $special, 'found' => true];
|
||||
}
|
||||
|
||||
// 2) 中国大陆号码:+86/0086 开头 或 11位以 1 开头
|
||||
if (isChinaMobile($digits, $hasPlus)) {
|
||||
$geo = lookupChinaPhoneDat($digits);
|
||||
if ($geo !== null) {
|
||||
return [
|
||||
'country' => '中国',
|
||||
'province' => $geo['province'],
|
||||
'city' => $geo['city'],
|
||||
'label' => '中国-' . $geo['province'] . ($geo['city'] && $geo['city'] !== $geo['province'] ? '-' . $geo['city'] : ''),
|
||||
'found' => true,
|
||||
];
|
||||
}
|
||||
return ['country' => '中国', 'province' => '', 'city' => '', 'label' => '中国', 'found' => false];
|
||||
}
|
||||
|
||||
// 3) 海外号码:识别国籍(国家区号表)
|
||||
$country = lookupCountryByCode($digits);
|
||||
if ($country !== null) {
|
||||
return ['country' => $country, 'province' => '', 'city' => '', 'label' => $country, 'found' => true];
|
||||
}
|
||||
|
||||
return ['country' => '', 'province' => '', 'city' => '', 'label' => '未知', 'found' => false];
|
||||
}
|
||||
|
||||
/** 港澳台特殊识别 */
|
||||
function specialRegion($digits)
|
||||
{
|
||||
// 852 香港 / 853 澳门 / 886 台湾(号码 8-9 位)
|
||||
if (strpos($digits, '852') === 0 && strlen($digits) >= 11) return '中国香港';
|
||||
if (strpos($digits, '853') === 0 && strlen($digits) >= 11) return '中国澳门';
|
||||
if (strpos($digits, '886') === 0 && strlen($digits) >= 12) return '中国台湾';
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 是否中国大陆手机号 */
|
||||
function isChinaMobile($digits, $hasPlus)
|
||||
{
|
||||
// +86 或 0086 开头(去除前缀后为 11 位 1 开头)
|
||||
if (strpos($digits, '86') === 0 && strlen($digits) === 13) {
|
||||
return preg_match('/^861[3-9]\d{9}$/', $digits) === 1;
|
||||
}
|
||||
// 纯 11 位 1 开头(无国际前缀)
|
||||
if (!$hasPlus && strlen($digits) === 11 && $digits[0] === '1') {
|
||||
return preg_match('/^1[3-9]\d{9}$/', $digits) === 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 使用 phone.dat 查询国内省市 */
|
||||
function lookupChinaPhoneDat($digits)
|
||||
{
|
||||
static $loader = null;
|
||||
if ($loader === null) {
|
||||
$loader = new PhoneDatLoader(__DIR__ . '/../../data/phone.dat');
|
||||
}
|
||||
// 提取 11 位号码中的前 7 位号段(如 1381234);带 86 前缀先去前缀
|
||||
$local = preg_replace('/^86/', '', $digits);
|
||||
$prefix7 = substr($local, 0, 7);
|
||||
if (strlen($prefix7) !== 7 || !preg_match('/^\d{7}$/', $prefix7)) {
|
||||
return null;
|
||||
}
|
||||
return $loader->find($prefix7);
|
||||
}
|
||||
|
||||
/** 海外号码:国家区号表识别国籍 */
|
||||
function lookupCountryByCode($digits)
|
||||
{
|
||||
static $map = null;
|
||||
if ($map === null) {
|
||||
$map = require __DIR__ . '/country_codes.php';
|
||||
}
|
||||
// 依次尝试 3 位 / 2 位 / 1 位区号
|
||||
for ($len = 3; $len >= 1; $len--) {
|
||||
$code = substr($digits, 0, $len);
|
||||
if (isset($map[$code])) {
|
||||
return $map[$code];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ==================== phone.dat 解析器(PHP 移植) ==================== */
|
||||
|
||||
/**
|
||||
* phone.dat 文件解析器
|
||||
* 文件格式(与 Python phone 库一致):
|
||||
* - 头部:4字节版本号 + 4字节首条记录偏移
|
||||
* - 记录:每条 9 字节 = 4字节号段(int) + 4字节数据偏移(int) + 1字节号码类型
|
||||
* - 数据区:省份|城市|邮编|区号 (竖线分隔,\0 结尾)
|
||||
*/
|
||||
class PhoneDatLoader
|
||||
{
|
||||
private $buf;
|
||||
private $firstOffset;
|
||||
private $count;
|
||||
|
||||
public function __construct($file)
|
||||
{
|
||||
$this->buf = file_get_contents($file);
|
||||
if ($this->buf === false || strlen($this->buf) < 8) {
|
||||
$this->buf = '';
|
||||
$this->firstOffset = 0;
|
||||
$this->count = 0;
|
||||
return;
|
||||
}
|
||||
$head = unpack('a4version/ioffset', substr($this->buf, 0, 8));
|
||||
$this->firstOffset = $head['offset'];
|
||||
$this->count = (int)((strlen($this->buf) - $this->firstOffset) / 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 7 位号段查询
|
||||
* @param string $prefix7 如 "1381234"
|
||||
* @return array|null ['province'=>, 'city'=>, 'zip_code'=>, 'area_code'=>, 'phone_type'=>]
|
||||
*/
|
||||
public function find($prefix7)
|
||||
{
|
||||
if ($this->count <= 0 || !preg_match('/^\d{7}$/', (string)$prefix7)) {
|
||||
return null;
|
||||
}
|
||||
$intPhone = (int)$prefix7;
|
||||
$left = 0;
|
||||
$right = $this->count;
|
||||
$buflen = strlen($this->buf);
|
||||
|
||||
while ($left <= $right) {
|
||||
$middle = (int)(($left + $right) / 2);
|
||||
$offset = $this->firstOffset + $middle * 9;
|
||||
if ($offset >= $buflen) {
|
||||
return null;
|
||||
}
|
||||
$rec = unpack('iprefix/irecord_offset/Ctype', substr($this->buf, $offset, 9));
|
||||
if ($rec['prefix'] > $intPhone) {
|
||||
$right = $middle - 1;
|
||||
} elseif ($rec['prefix'] < $intPhone) {
|
||||
$left = $middle + 1;
|
||||
} else {
|
||||
$content = $this->readRecord($rec['record_offset']);
|
||||
if ($content === null) {
|
||||
return null;
|
||||
}
|
||||
$parts = explode('|', $content);
|
||||
return [
|
||||
'province' => $parts[0] ?? '',
|
||||
'city' => $parts[1] ?? '',
|
||||
'zip_code' => $parts[2] ?? '',
|
||||
'area_code' => $parts[3] ?? '',
|
||||
'phone_type' => $this->typeName($rec['type']),
|
||||
];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function readRecord($offset)
|
||||
{
|
||||
$end = strpos($this->buf, "\x00", $offset);
|
||||
if ($end === false) {
|
||||
return null;
|
||||
}
|
||||
return substr($this->buf, $offset, $end - $offset);
|
||||
}
|
||||
|
||||
private function typeName($no)
|
||||
{
|
||||
$map = [
|
||||
1 => '移动', 2 => '联通', 3 => '电信',
|
||||
4 => '电信虚拟运营商', 5 => '联通虚拟运营商', 6 => '移动虚拟运营商',
|
||||
7 => '广电', 8 => '广电虚拟运营商',
|
||||
];
|
||||
return $map[$no] ?? '未知';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user