This commit is contained in:
nanguaboss
2026-08-06 00:50:23 +08:00
parent 595520822a
commit 2cb688ed46
29 changed files with 5227 additions and 186 deletions
+227
View File
@@ -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' => '乌兹别克斯坦',
];
+45
View File
@@ -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);
+4 -2
View File
@@ -14,6 +14,7 @@ const COMPANY_FIELDS = [
'industry' => 's', 'industry_subdivision' => 's', 'industry' => 's', 'industry_subdivision' => 's',
'latest_employee_count' => 'i', 'latest_annual_revenue' => 's', 'latest_employee_count' => 'i', 'latest_annual_revenue' => 's',
'is_listed' => 'i', 'stock_code' => 's', 'website' => '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', 'union_id' => 's', 'full_name' => 's', 'gender' => 's', 'nationality' => 's',
'id_type' => 's', 'id_number' => 's', 'education' => 's', 'graduated_from' => 's', 'id_type' => 's', 'id_number' => 's', 'education' => 's', 'graduated_from' => 's',
'hometown' => 's', 'work_location' => 's', 'hometown' => 's', 'work_location' => 's',
'source_channel' => 's', 'source_detail' => 's',
]; ];
/** 媒体(社交账号)表可写入字段白名单 */ /** 媒体(社交账号)表可写入字段白名单 */
const MEDIA_FIELDS = [ const MEDIA_FIELDS = [
'owner_type' => 's', 'owner_id' => 'i', 'platform' => 's', 'account_id' => 's', '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; 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( $ins = $pdo->prepare(
"INSERT INTO company_certifications "INSERT INTO company_certifications
(company_id, certification_type_id, certificate_number, issue_date, expiry_date) (company_id, certification_type_id, certificate_number, issue_date, expiry_date)
+229
View File
@@ -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] ?? '未知';
}
}
+2 -1
View File
@@ -2,6 +2,7 @@
/** /**
* 认证类型字典接口 GET /api/company/cert_types.php * 认证类型字典接口 GET /api/company/cert_types.php
* 返回启用中的认证类型(如:瞪羚企业、国家高新技术企业等),供编辑弹窗下拉使用 * 返回启用中的认证类型(如:瞪羚企业、国家高新技术企业等),供编辑弹窗下拉使用
* 数据源:date_dict_certificate(原 certification_types,v1.0.16 改名)
*/ */
require_once __DIR__ . '/../common/db.php'; require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php'; require_once __DIR__ . '/../common/response.php';
@@ -10,6 +11,6 @@ require_once __DIR__ . '/../common/auth.php';
checkPermission('company'); checkPermission('company');
$pdo = DB::getInstance()->getPdo(); $pdo = DB::getInstance()->getPdo();
$types = $pdo->query("SELECT id, name FROM certification_types WHERE is_active = 1 ORDER BY sort_order, id")->fetchAll(); $types = $pdo->query("SELECT id, name FROM date_dict_certificate WHERE is_active = 1 ORDER BY sort_order, id")->fetchAll();
Response::success(['types' => $types]); Response::success(['types' => $types]);
+13 -7
View File
@@ -1,7 +1,7 @@
<?php <?php
/** /**
* 企业详情接口 GET /api/company/detail.php?id=1 * 企业详情接口 GET /api/company/detail.php?id=1
* 返回企业主表信息 + 关联数据(产品品类/需求/文档/财务) * 返回企业主表信息 + 关联数据(产品品类/文档/财务/资质/联系方式)
*/ */
require_once __DIR__ . '/../common/db.php'; require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php'; require_once __DIR__ . '/../common/response.php';
@@ -26,9 +26,6 @@ if (!$company) {
$products = $pdo->prepare("SELECT id, category_name, category_description, is_core, is_active FROM company_products WHERE company_id = ? AND is_active = 1"); $products = $pdo->prepare("SELECT id, category_name, category_description, is_core, is_active FROM company_products WHERE company_id = ? AND is_active = 1");
$products->execute([$id]); $products->execute([$id]);
$needs = $pdo->prepare("SELECT id, contact_person, need_category, target_product_category, application_scenario, description, is_valid, created_at FROM company_needs WHERE company_id = ? ORDER BY id DESC");
$needs->execute([$id]);
$docs = $pdo->prepare( $docs = $pdo->prepare(
"SELECT cd.id, cd.doc_name, cd.storage_path, cd.file_type, cd.publish_date, cd.is_active "SELECT cd.id, cd.doc_name, cd.storage_path, cd.file_type, cd.publish_date, cd.is_active
FROM company_documents cd FROM company_documents cd
@@ -53,13 +50,22 @@ $certs = $pdo->prepare(
"SELECT cc.certification_type_id, ct.name AS certification_name, cc.certificate_number, "SELECT cc.certification_type_id, ct.name AS certification_name, cc.certificate_number,
cc.issue_date, cc.expiry_date cc.issue_date, cc.expiry_date
FROM company_certifications cc FROM company_certifications cc
INNER JOIN certification_types ct ON ct.id = cc.certification_type_id INNER JOIN date_dict_certificate ct ON ct.id = cc.certification_type_id
WHERE cc.company_id = ? WHERE cc.company_id = ?
ORDER BY ct.sort_order, ct.id" ORDER BY ct.sort_order, ct.id"
); );
$certs->execute([$id]); $certs->execute([$id]);
$certTypes = $pdo->query("SELECT id, name FROM certification_types WHERE is_active = 1 ORDER BY sort_order, id")->fetchAll(); $certTypes = $pdo->query("SELECT id, name FROM date_dict_certificate WHERE is_active = 1 ORDER BY sort_order, id")->fetchAll();
// 联系方式:social_accounts 中属于该公司的记录(仅展示,无修改/删除入口)
$accounts = $pdo->prepare(
"SELECT id, platform, account_id, profile_url, remark, is_primary, is_defult, is_active, created_at
FROM social_accounts
WHERE owner_type = 'company' AND owner_id = ?
ORDER BY is_defult DESC, is_primary DESC, id DESC"
);
$accounts->execute([$id]);
// 关联联系人(工作经历 + 联系方式) // 关联联系人(工作经历 + 联系方式)
$contacts = $pdo->prepare( $contacts = $pdo->prepare(
@@ -79,11 +85,11 @@ $contacts->execute([$id, $id]);
Response::success([ Response::success([
'company' => $company, 'company' => $company,
'products' => $products->fetchAll(), 'products' => $products->fetchAll(),
'needs' => $needs->fetchAll(),
'documents' => $docs->fetchAll(), 'documents' => $docs->fetchAll(),
'financials' => $financials->fetchAll(), 'financials' => $financials->fetchAll(),
'relations' => $relations->fetchAll(), 'relations' => $relations->fetchAll(),
'contacts' => $contacts->fetchAll(), 'contacts' => $contacts->fetchAll(),
'accounts' => $accounts->fetchAll(),
'certifications' => $certs->fetchAll(), 'certifications' => $certs->fetchAll(),
'certification_types' => $certTypes, 'certification_types' => $certTypes,
]); ]);
+2 -2
View File
@@ -60,10 +60,10 @@ $stmt = $pdo->prepare(
pwe.job_level, pwe.start_date, pwe.end_date, pwe.is_current, pwe.job_level, pwe.start_date, pwe.end_date, pwe.is_current,
(SELECT sa.account_id FROM social_accounts sa (SELECT sa.account_id FROM social_accounts sa
WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.platform = 'phone' AND sa.is_active = 1 WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.platform = 'phone' AND sa.is_active = 1
ORDER BY sa.is_primary DESC LIMIT 1) AS phone, ORDER BY sa.is_defult DESC, sa.is_primary DESC LIMIT 1) AS phone,
(SELECT sa.account_id FROM social_accounts sa (SELECT sa.account_id FROM social_accounts sa
WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.platform = 'email' AND sa.is_active = 1 WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.platform = 'email' AND sa.is_active = 1
ORDER BY sa.is_primary DESC LIMIT 1) AS email ORDER BY sa.is_defult DESC, sa.is_primary DESC LIMIT 1) AS email
FROM person_work_experiences pwe FROM person_work_experiences pwe
INNER JOIN persons p ON p.id = pwe.person_id INNER JOIN persons p ON p.id = pwe.person_id
WHERE $whereSql WHERE $whereSql
+3 -3
View File
@@ -32,7 +32,7 @@ $stmt = $pdo->prepare(
"SELECT id, name_zh, name_en, business_role, country, registration_number, "SELECT id, name_zh, name_en, business_role, country, registration_number,
address, legal_representative, industry, industry_subdivision, website, address, legal_representative, industry, industry_subdivision, website,
registered_capital, established_date, latest_employee_count, latest_annual_revenue, registered_capital, established_date, latest_employee_count, latest_annual_revenue,
is_listed, stock_code, created_at is_listed, stock_code, source_channel, source_detail, created_at
FROM companies WHERE id IN ($in) AND is_active = 1 ORDER BY id DESC" FROM companies WHERE id IN ($in) AND is_active = 1 ORDER BY id DESC"
); );
$stmt->execute($idList); $stmt->execute($idList);
@@ -44,14 +44,14 @@ header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="companies_' . date('Ymd_His') . '.csv"'); header('Content-Disposition: attachment; filename="companies_' . date('Ymd_His') . '.csv"');
echo "\xEF\xBB\xBF"; // UTF-8 BOM,便于 Excel 直接打开 echo "\xEF\xBB\xBF"; // UTF-8 BOM,便于 Excel 直接打开
$out = fopen('php://output', 'w'); $out = fopen('php://output', 'w');
fputcsv($out, ['ID', '公司名称', '英文名称', '业务角色', '国家/地区', '注册号', '地址', '法人', '行业', '行业细分', '官网', '注册资本', '成立日期', '员工数', '年营收', '是否上市', '股票代码', '创建时间']); fputcsv($out, ['ID', '公司名称', '英文名称', '业务角色', '国家/地区', '注册号', '地址', '法人', '行业', '行业细分', '官网', '注册资本', '成立日期', '员工数', '年营收', '是否上市', '股票代码', '来源渠道', '来源详情', '创建时间']);
foreach ($list as $r) { foreach ($list as $r) {
fputcsv($out, [ fputcsv($out, [
$r['id'], $r['name_zh'], $r['name_en'], $r['business_role'], $r['id'], $r['name_zh'], $r['name_en'], $r['business_role'],
$r['country'], $r['registration_number'], $r['address'], $r['legal_representative'], $r['country'], $r['registration_number'], $r['address'], $r['legal_representative'],
$r['industry'], $r['industry_subdivision'], $r['website'], $r['registered_capital'], $r['industry'], $r['industry_subdivision'], $r['website'], $r['registered_capital'],
$r['established_date'], $r['latest_employee_count'], $r['latest_annual_revenue'], $r['established_date'], $r['latest_employee_count'], $r['latest_annual_revenue'],
$r['is_listed'], $r['stock_code'], date('Y-m-d', strtotime($r['created_at'])), $r['is_listed'], $r['stock_code'], $r['source_channel'], $r['source_detail'], date('Y-m-d', strtotime($r['created_at'])),
]); ]);
} }
fclose($out); fclose($out);
+4 -2
View File
@@ -36,6 +36,7 @@ $map = [
'industry' => 'industry', 'industry_subdivision' => 'industry_subdivision', 'website' => 'website', 'industry' => 'industry', 'industry_subdivision' => 'industry_subdivision', 'website' => 'website',
'registered_capital' => 'registered_capital', 'established_date' => 'established_date', 'registered_capital' => 'registered_capital', 'established_date' => 'established_date',
'is_listed' => 'is_listed', 'stock_code' => 'stock_code', 'is_listed' => 'is_listed', 'stock_code' => 'stock_code',
'source_channel' => 'source_channel', 'source_detail' => 'source_detail',
]; ];
$pdo = DB::getInstance()->getPdo(); $pdo = DB::getInstance()->getPdo();
@@ -46,8 +47,8 @@ $stmt = $pdo->prepare(
"INSERT INTO companies "INSERT INTO companies
(name_zh, name_en, display_name, business_role, country, registration_number, (name_zh, name_en, display_name, business_role, country, registration_number,
address, legal_form, legal_representative, industry, industry_subdivision, website, address, legal_form, legal_representative, industry, industry_subdivision, website,
registered_capital, established_date, is_listed, stock_code) registered_capital, established_date, is_listed, stock_code, source_channel, source_detail)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
); );
while (($row = fgetcsv($handle)) !== false) { while (($row = fgetcsv($handle)) !== false) {
@@ -72,6 +73,7 @@ while (($row = fgetcsv($handle)) !== false) {
$rec['industry'] ?? null, $rec['industry_subdivision'] ?? null, $rec['website'] ?? null, $rec['industry'] ?? null, $rec['industry_subdivision'] ?? null, $rec['website'] ?? null,
$rec['registered_capital'] ?? null, $rec['established_date'], $rec['registered_capital'] ?? null, $rec['established_date'],
($rec['is_listed'] ?? 0) ? 1 : 0, $rec['stock_code'] ?? null, ($rec['is_listed'] ?? 0) ? 1 : 0, $rec['stock_code'] ?? null,
$rec['source_channel'] ?? null, $rec['source_detail'] ?? null,
]); ]);
$inserted++; $inserted++;
} catch (Exception $e) { } catch (Exception $e) {
+1 -1
View File
@@ -55,7 +55,7 @@ $stmt = $pdo->prepare(
"SELECT id, name_zh, name_en, display_name, business_role, country, registration_number, "SELECT id, name_zh, name_en, display_name, business_role, country, registration_number,
address, legal_representative, industry, industry_subdivision, website, address, legal_representative, industry, industry_subdivision, website,
latest_employee_count, latest_annual_revenue, is_listed, stock_code, latest_employee_count, latest_annual_revenue, is_listed, stock_code,
is_active, created_at, updated_at source_channel, source_detail, is_active, created_at, updated_at
FROM companies FROM companies
WHERE $whereSql WHERE $whereSql
ORDER BY id DESC ORDER BY id DESC
+3 -3
View File
@@ -32,7 +32,7 @@ $whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo(); $pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"SELECT sa.id, sa.owner_type, sa.owner_id, sa.platform, sa.account_id, sa.profile_url, sa.remark, "SELECT sa.id, sa.owner_type, sa.owner_id, sa.platform, sa.account_id, sa.profile_url, sa.remark, sa.is_defult,
mca.account_level, mca.content_categories, mca.follower_count, mca.avg_read_count, mca.account_level, mca.content_categories, mca.follower_count, mca.avg_read_count,
mca.certification_type, sa.created_at, mca.certification_type, sa.created_at,
CASE sa.owner_type CASE sa.owner_type
@@ -52,11 +52,11 @@ header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="media_' . date('Ymd_His') . '.csv"'); header('Content-Disposition: attachment; filename="media_' . date('Ymd_His') . '.csv"');
echo "\xEF\xBB\xBF"; echo "\xEF\xBB\xBF";
$out = fopen('php://output', 'w'); $out = fopen('php://output', 'w');
fputcsv($out, ['ID', '归属类型', '归属ID', '归属名称', '平台', '账号', '主页链接', '备注', '等级', '内容领域', '粉丝数', '平均阅读', '认证类型', '创建时间']); fputcsv($out, ['ID', '归属类型', '归属ID', '归属名称', '平台', '账号', '主页链接', '备注', '是否常用', '等级', '内容领域', '粉丝数', '平均阅读', '认证类型', '创建时间']);
foreach ($list as $r) { foreach ($list as $r) {
fputcsv($out, [ fputcsv($out, [
$r['id'], $r['owner_type'], $r['owner_id'], $r['owner_name'], $r['platform'], $r['id'], $r['owner_type'], $r['owner_id'], $r['owner_name'], $r['platform'],
$r['account_id'], $r['profile_url'], $r['remark'], $r['account_level'], $r['account_id'], $r['profile_url'], $r['remark'], $r['is_defult'] ? '常用' : '备用', $r['account_level'],
$r['content_categories'], $r['follower_count'], $r['avg_read_count'], $r['content_categories'], $r['follower_count'], $r['avg_read_count'],
$r['certification_type'], $r['created_at'], $r['certification_type'], $r['created_at'],
]); ]);
+4 -3
View File
@@ -1,7 +1,7 @@
<?php <?php
/** /**
* 媒体数据 CSV 导入接口 POST /api/media/import.php (multipart/form-data, 字段名 file) * 媒体数据 CSV 导入接口 POST /api/media/import.php (multipart/form-data, 字段名 file)
* CSV 表头:owner_type,owner_id,platform,account_id,profile_url,remark,is_primary, * CSV 表头:owner_type,owner_id,platform,account_id,profile_url,remark,is_primary,is_defult,
* account_level,content_categories,follower_count,avg_read_count,certification_type * account_level,content_categories,follower_count,avg_read_count,certification_type
* owner_type 为 person/company 时 owner_id 也可填对应名称(display_name/full_name),自动解析为ID。 * owner_type 为 person/company 时 owner_id 也可填对应名称(display_name/full_name),自动解析为ID。
*/ */
@@ -26,8 +26,8 @@ $header = str_getcsv(trim($first));
$pdo = DB::getInstance()->getPdo(); $pdo = DB::getInstance()->getPdo();
$insAccount = $pdo->prepare( $insAccount = $pdo->prepare(
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary, is_active) "INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary, is_defult, is_active)
VALUES (?,?,?,?,?,?,?,1)" VALUES (?,?,?,?,?,?,?,?,1)"
); );
$insAttr = $pdo->prepare( $insAttr = $pdo->prepare(
"INSERT INTO media_commercial_attributes "INSERT INTO media_commercial_attributes
@@ -75,6 +75,7 @@ while (($row = fgetcsv($handle)) !== false) {
$insAccount->execute([ $insAccount->execute([
$ownerType, $ownerId, $rec['platform'], $rec['account_id'], $ownerType, $ownerId, $rec['platform'], $rec['account_id'],
$rec['profile_url'] ?? null, $rec['remark'] ?? null, !empty($rec['is_primary']) ? 1 : 0, $rec['profile_url'] ?? null, $rec['remark'] ?? null, !empty($rec['is_primary']) ? 1 : 0,
!empty($rec['is_defult']) ? 1 : 0,
]); ]);
$newId = (int)$pdo->lastInsertId(); $newId = (int)$pdo->lastInsertId();
if (!empty($rec['account_level']) || !empty($rec['certification_type'])) { if (!empty($rec['account_level']) || !empty($rec['certification_type'])) {
+1 -1
View File
@@ -51,7 +51,7 @@ $total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit; $offset = ($page - 1) * $limit;
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"SELECT sa.id, sa.owner_type, sa.owner_id, sa.platform, sa.account_id, sa.profile_url, "SELECT sa.id, sa.owner_type, sa.owner_id, sa.platform, sa.account_id, sa.profile_url,
sa.remark, sa.is_primary, sa.is_active, sa.created_at, sa.remark, sa.is_primary, sa.is_defult, sa.is_active, sa.created_at,
mca.account_level, mca.content_categories, mca.follower_count, mca.avg_read_count, mca.account_level, mca.content_categories, mca.follower_count, mca.avg_read_count,
mca.certification_type, mca.special_requirements, mca.media_remark, mca.certification_type, mca.special_requirements, mca.media_remark,
CASE sa.owner_type CASE sa.owner_type
+4 -4
View File
@@ -34,18 +34,18 @@ if ((int)$chk->fetchColumn() > 0) {
$pdo->prepare("INSERT INTO persons $sql")->execute($params); $pdo->prepare("INSERT INTO persons $sql")->execute($params);
$newId = (int)$pdo->lastInsertId(); $newId = (int)$pdo->lastInsertId();
// 写入联系方式 // 写入联系方式(v1.0.16:支持 is_defult 字段,1常用/0备用)
$contacts = json_decode($_POST['contacts'] ?? '[]', true); $contacts = json_decode($_POST['contacts'] ?? '[]', true);
if (is_array($contacts)) { if (is_array($contacts)) {
$ins = $pdo->prepare( $ins = $pdo->prepare(
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary) "INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary, is_defult)
VALUES ('person', ?, ?, ?, ?, ?, ?)" VALUES ('person', ?, ?, ?, ?, ?, ?, ?)"
); );
foreach ($contacts as $c) { foreach ($contacts as $c) {
$platform = trim($c['platform'] ?? ''); $platform = trim($c['platform'] ?? '');
$accountId = trim($c['account_id'] ?? ''); $accountId = trim($c['account_id'] ?? '');
if ($platform === '' || $accountId === '') continue; if ($platform === '' || $accountId === '') continue;
$ins->execute([$newId, $platform, $accountId, $c['profile_url'] ?? null, $c['remark'] ?? null, !empty($c['is_primary']) ? 1 : 0]); $ins->execute([$newId, $platform, $accountId, $c['profile_url'] ?? null, $c['remark'] ?? null, !empty($c['is_primary']) ? 1 : 0, !empty($c['is_defult']) ? 1 : 0]);
} }
} }
+25 -2
View File
@@ -1,6 +1,7 @@
<?php <?php
/** /**
* 人员详情接口 GET /api/person/detail.php?id=1 * 人员详情接口 GET /api/person/detail.php?id=1
* v1.0.16:联系方式改为“手机”“邮箱”两字段(仅常用);另有全部账号列表 accounts 供编辑回显
*/ */
require_once __DIR__ . '/../common/db.php'; require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php'; require_once __DIR__ . '/../common/response.php';
@@ -22,8 +23,24 @@ if (!$person) {
Response::error('人员不存在'); Response::error('人员不存在');
} }
$accounts = $pdo->prepare("SELECT id, platform, account_id, profile_url, remark, is_primary, is_active FROM social_accounts WHERE owner_type = 'person' AND owner_id = ? ORDER BY is_primary DESC, id DESC"); $accounts = $pdo->prepare("SELECT id, platform, account_id, profile_url, remark, is_primary, is_defult, is_active FROM social_accounts WHERE owner_type = 'person' AND owner_id = ? ORDER BY is_defult DESC, is_primary DESC, id DESC");
$accounts->execute([$id]); $accounts->execute([$id]);
$accountList = $accounts->fetchAll();
// 常用手机/邮箱(优先 is_defult=1,其次 is_primary=1)
$phone = '';
$email = '';
foreach ($accountList as $a) {
if ($phone === '' && $a['platform'] === 'phone') {
$phone = $a['account_id'];
}
if ($email === '' && $a['platform'] === 'email') {
$email = $a['account_id'];
}
if ($phone !== '' && $email !== '') {
break;
}
}
$experiences = $pdo->prepare( $experiences = $pdo->prepare(
"SELECT pwe.id, pwe.company_id, c.display_name, c.industry, pwe.position, pwe.department, "SELECT pwe.id, pwe.company_id, c.display_name, c.industry, pwe.position, pwe.department,
@@ -35,4 +52,10 @@ $experiences = $pdo->prepare(
); );
$experiences->execute([$id]); $experiences->execute([$id]);
Response::success(['person' => $person, 'accounts' => $accounts->fetchAll(), 'experiences' => $experiences->fetchAll()]); Response::success([
'person' => $person,
'accounts' => $accountList,
'phone' => $phone,
'email' => $email,
'experiences' => $experiences->fetchAll(),
]);
+5 -5
View File
@@ -33,9 +33,9 @@ $whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo(); $pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"SELECT p.id, p.union_id, p.full_name, p.gender, p.nationality, p.id_type, p.id_number, "SELECT p.id, p.union_id, p.full_name, p.gender, p.nationality, p.id_type, p.id_number,
p.education, p.graduated_from, p.hometown, p.work_location, p.created_at, p.education, p.graduated_from, p.hometown, p.work_location, p.source_channel, p.source_detail, p.created_at,
(SELECT sa.account_id FROM social_accounts sa WHERE sa.owner_type='person' AND sa.owner_id=p.id AND sa.platform='phone' AND sa.is_active=1 LIMIT 1) AS phone, (SELECT sa.account_id FROM social_accounts sa WHERE sa.owner_type='person' AND sa.owner_id=p.id AND sa.platform='phone' AND sa.is_active=1 ORDER BY sa.is_defult DESC, sa.is_primary DESC LIMIT 1) AS phone,
(SELECT sa.account_id FROM social_accounts sa WHERE sa.owner_type='person' AND sa.owner_id=p.id AND sa.platform='email' AND sa.is_active=1 LIMIT 1) AS email (SELECT sa.account_id FROM social_accounts sa WHERE sa.owner_type='person' AND sa.owner_id=p.id AND sa.platform='email' AND sa.is_active=1 ORDER BY sa.is_defult DESC, sa.is_primary DESC LIMIT 1) AS email
FROM persons p WHERE $whereSql ORDER BY p.id DESC" FROM persons p WHERE $whereSql ORDER BY p.id DESC"
); );
$stmt->execute($params); $stmt->execute($params);
@@ -47,12 +47,12 @@ header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="persons_' . date('Ymd_His') . '.csv"'); header('Content-Disposition: attachment; filename="persons_' . date('Ymd_His') . '.csv"');
echo "\xEF\xBB\xBF"; echo "\xEF\xBB\xBF";
$out = fopen('php://output', 'w'); $out = fopen('php://output', 'w');
fputcsv($out, ['ID', 'union_id', '姓名', '性别', '国籍', '证件类型', '证件号', '学历', '毕业院校', '家乡', '工作所在地', '手机', '邮箱', '创建时间']); fputcsv($out, ['ID', 'union_id', '姓名', '性别', '国籍', '证件类型', '证件号', '学历', '毕业院校', '家乡', '工作所在地', '来源渠道', '来源详情', '手机', '邮箱', '创建时间']);
foreach ($list as $r) { foreach ($list as $r) {
fputcsv($out, [ fputcsv($out, [
$r['id'], $r['union_id'], $r['full_name'], $r['gender'], $r['nationality'], $r['id'], $r['union_id'], $r['full_name'], $r['gender'], $r['nationality'],
$r['id_type'], $r['id_number'], $r['education'], $r['graduated_from'], $r['id_type'], $r['id_number'], $r['education'], $r['graduated_from'],
$r['hometown'], $r['work_location'], $r['phone'], $r['email'], $r['created_at'], $r['hometown'], $r['work_location'], $r['source_channel'], $r['source_detail'], $r['phone'], $r['email'], $r['created_at'],
]); ]);
} }
fclose($out); fclose($out);
+9 -8
View File
@@ -2,8 +2,8 @@
/** /**
* 人员 CSV 导入接口 POST /api/person/import.php (multipart/form-data, 字段名 file) * 人员 CSV 导入接口 POST /api/person/import.php (multipart/form-data, 字段名 file)
* CSV 表头:union_id,full_name,gender,nationality,id_type,id_number,education, * CSV 表头:union_id,full_name,gender,nationality,id_type,id_number,education,
* graduated_from,hometown,work_location,phone,email * graduated_from,hometown,work_location,phone,email,source_channel,source_detail
* 仅 full_name 必填;union_id 留空自动生成;phone/email 写入 social_accounts。 * 仅 full_name 必填;union_id 留空自动生成;phone/email 写入 social_accounts(默认常用)。
*/ */
require_once __DIR__ . '/../common/db.php'; require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php'; require_once __DIR__ . '/../common/response.php';
@@ -26,12 +26,12 @@ $header = str_getcsv(trim($first));
$pdo = DB::getInstance()->getPdo(); $pdo = DB::getInstance()->getPdo();
$insPerson = $pdo->prepare( $insPerson = $pdo->prepare(
"INSERT INTO persons (union_id, full_name, gender, nationality, id_type, id_number, education, graduated_from, hometown, work_location) "INSERT INTO persons (union_id, full_name, gender, nationality, id_type, id_number, education, graduated_from, hometown, work_location, source_channel, source_detail)
VALUES (?,?,?,?,?,?,?,?,?,?)" VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
); );
$insAccount = $pdo->prepare( $insAccount = $pdo->prepare(
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, is_primary) "INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, is_defult)
VALUES ('person', ?, ?, ?, ?)" VALUES ('person', ?, ?, ?, 1)"
); );
$inserted = 0; $inserted = 0;
@@ -63,13 +63,14 @@ while (($row = fgetcsv($handle)) !== false) {
$unionId, $rec['full_name'], $rec['gender'] ?? '保密', $rec['nationality'] ?? null, $unionId, $rec['full_name'], $rec['gender'] ?? '保密', $rec['nationality'] ?? null,
$rec['id_type'] ?? null, $rec['id_number'] ?? null, $rec['education'] ?? null, $rec['id_type'] ?? null, $rec['id_number'] ?? null, $rec['education'] ?? null,
$rec['graduated_from'] ?? null, $rec['hometown'] ?? null, $rec['work_location'] ?? null, $rec['graduated_from'] ?? null, $rec['hometown'] ?? null, $rec['work_location'] ?? null,
$rec['source_channel'] ?? null, $rec['source_detail'] ?? null,
]); ]);
$newId = (int)$pdo->lastInsertId(); $newId = (int)$pdo->lastInsertId();
if (!empty($rec['phone'])) { if (!empty($rec['phone'])) {
$insAccount->execute([$newId, 'phone', $rec['phone'], 1]); $insAccount->execute([$newId, 'phone', $rec['phone']]);
} }
if (!empty($rec['email'])) { if (!empty($rec['email'])) {
$insAccount->execute([$newId, 'email', $rec['email'], 0]); $insAccount->execute([$newId, 'email', $rec['email']]);
} }
$inserted++; $inserted++;
} catch (Exception $e) { } catch (Exception $e) {
+12 -2
View File
@@ -3,6 +3,7 @@
* 人员列表接口 GET/POST /api/person/list.php * 人员列表接口 GET/POST /api/person/list.php
* 参数:page / limit / keyword(姓名/证件号/手机/邮箱)/ gender / nationality / work_location / is_active * 参数:page / limit / keyword(姓名/证件号/手机/邮箱)/ gender / nationality / work_location / is_active
* 返回:{ list, total, page, limit } * 返回:{ list, total, page, limit }
* v1.0.16:联系方式列改为“手机”“邮箱”两个字段(仅返回常用:is_defult=1 优先,其次 is_primary=1)
*/ */
require_once __DIR__ . '/../common/db.php'; require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php'; require_once __DIR__ . '/../common/response.php';
@@ -44,12 +45,21 @@ $stmt = $pdo->prepare("SELECT COUNT(*) FROM persons p WHERE $whereSql");
$stmt->execute($params); $stmt->execute($params);
$total = (int)$stmt->fetchColumn(); $total = (int)$stmt->fetchColumn();
// 常用手机/邮箱:优先 is_defult=1,其次 is_primary=1,最后取任意一条
$phoneSql = "(SELECT sa.account_id FROM social_accounts sa
WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.platform = 'phone' AND sa.is_active = 1
ORDER BY sa.is_defult DESC, sa.is_primary DESC, sa.id DESC LIMIT 1)";
$emailSql = "(SELECT sa.account_id FROM social_accounts sa
WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.platform = 'email' AND sa.is_active = 1
ORDER BY sa.is_defult DESC, sa.is_primary DESC, sa.id DESC LIMIT 1)";
$offset = ($page - 1) * $limit; $offset = ($page - 1) * $limit;
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"SELECT p.id, p.union_id, p.full_name, p.gender, p.nationality, p.id_type, p.id_number, "SELECT p.id, p.union_id, p.full_name, p.gender, p.nationality, p.id_type, p.id_number,
p.education, p.graduated_from, p.hometown, p.work_location, p.is_active, p.created_at, p.education, p.graduated_from, p.hometown, p.work_location, p.is_active, p.created_at,
(SELECT GROUP_CONCAT(CONCAT(sa.platform, ':', sa.account_id) SEPARATOR ' | ') p.source_channel, p.source_detail,
FROM social_accounts sa WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.is_active = 1) AS contacts $phoneSql AS phone,
$emailSql AS email
FROM persons p FROM persons p
WHERE $whereSql WHERE $whereSql
ORDER BY p.id DESC ORDER BY p.id DESC
+3 -3
View File
@@ -36,14 +36,14 @@ if (isset($_POST['contacts'])) {
$contacts = json_decode($_POST['contacts'], true); $contacts = json_decode($_POST['contacts'], true);
if (is_array($contacts)) { if (is_array($contacts)) {
$ins = $pdo->prepare( $ins = $pdo->prepare(
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary) "INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary, is_defult)
VALUES ('person', ?, ?, ?, ?, ?, ?)" VALUES ('person', ?, ?, ?, ?, ?, ?, ?)"
); );
foreach ($contacts as $c) { foreach ($contacts as $c) {
$platform = trim($c['platform'] ?? ''); $platform = trim($c['platform'] ?? '');
$accountId = trim($c['account_id'] ?? ''); $accountId = trim($c['account_id'] ?? '');
if ($platform === '' || $accountId === '') continue; if ($platform === '' || $accountId === '') continue;
$ins->execute([$id, $platform, $accountId, $c['profile_url'] ?? null, $c['remark'] ?? null, !empty($c['is_primary']) ? 1 : 0]); $ins->execute([$id, $platform, $accountId, $c['profile_url'] ?? null, $c['remark'] ?? null, !empty($c['is_primary']) ? 1 : 0, !empty($c['is_defult']) ? 1 : 0]);
} }
} }
} }
BIN
View File
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"certification_types": [
[
6,
"ISO14001认证"
],
[
7,
"ISO45001认证"
],
[
5,
"ISO9001认证"
],
[
2,
"专精特新企业"
],
[
4,
"制造业单项冠军"
],
[
1,
"国家高新技术企业"
],
[
3,
"瞪羚企业"
],
[
8,
"知识产权管理体系认证"
]
],
"company_certifications": []
}
+4014
View File
File diff suppressed because it is too large Load Diff
+140
View File
@@ -0,0 +1,140 @@
-- ============================================================
-- SuperLink Web 后台管理系统 - v1.0.16 数据库结构变更脚本
-- 对应需求文档《修改说明》:
-- 1) 所有数据表时间字段调整为"年-月-日"(DATETIME -> DATE)
-- 2) companies / persons 增加 source_channel、source_detail
-- 3) social_accounts 增加 is_defult(1常用 / 0不常用备用)
-- 4) 数据字典:certification_types 改名为 date_dict_certificate,
-- 新增 date_dict_country / date_dict_area / date_dict_industry / source_channels
-- 字典数据见:sql/dict_seed.sql(由官方 Excel 生成,需一并执行)
-- 执行方式:先执行本文件,再执行 sql/dict_seed.sql。
-- 提示:原库业务表(companies/persons/social_accounts 等)不在仓库中,
-- 时间字段批量转换建议使用 tools/migrate_time_to_date.php 自动处理。
-- ============================================================
SET NAMES utf8mb4;
-- ------------------------------------------------------------
-- 1) 时间字段调整(DATETIME -> DATE)
-- 以下为各业务表已知时间字段的手工 ALTER 示例(请按实际库结构核对列名):
-- companies.created_at / updated_at / established_date
-- persons.created_at / updated_at
-- social_accounts.created_at
-- company_needs.created_at
-- company_documents.publish_date / created_at
-- company_certifications.issue_date / expiry_date / created_at
-- company_relations.established_date / created_at
-- person_work_experiences.start_date / end_date / created_at
-- company_products.created_at
-- media_commercial_attributes.created_at
-- channels.created_at / updated_at
-- system_users.last_login_time / created_at / updated_at
-- system_roles.created_at
-- system_logs.created_at
--
-- 示例(MySQL 8.0.13+ 支持 DEFAULT (CURRENT_DATE);5.7 请去掉默认值改为 NULL 后由程序写入):
-- ALTER TABLE companies MODIFY COLUMN created_at DATE NULL DEFAULT (CURRENT_DATE);
-- ALTER TABLE companies MODIFY COLUMN updated_at DATE NULL DEFAULT (CURRENT_DATE);
-- 批量转换推荐直接运行:php tools/migrate_time_to_date.php (自动遍历 information_schema)
-- ------------------------------------------------------------
-- 2) companies / persons 增加来源字段
-- ------------------------------------------------------------
ALTER TABLE `companies`
ADD COLUMN `source_channel` VARCHAR(50) NULL DEFAULT NULL COMMENT '来源渠道(字典:source_channels,如展会/SNS/竞价广告)' AFTER `website`,
ADD COLUMN `source_detail` VARCHAR(255) NULL DEFAULT NULL COMMENT '来源详情(如:华南工博会)' AFTER `source_channel`;
ALTER TABLE `persons`
ADD COLUMN `source_channel` VARCHAR(50) NULL DEFAULT NULL COMMENT '来源渠道(字典:source_channels)' AFTER `work_location`,
ADD COLUMN `source_detail` VARCHAR(255) NULL DEFAULT NULL COMMENT '来源详情' AFTER `source_channel`;
-- ------------------------------------------------------------
-- 3) social_accounts 增加 is_defult(1常用 / 0不常用、备用)
-- ------------------------------------------------------------
ALTER TABLE `social_accounts`
ADD COLUMN `is_defult` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否常用:1常用,0不常用/备用' AFTER `is_primary`;
-- ------------------------------------------------------------
-- 4) 数据字典
-- ------------------------------------------------------------
-- 4.1 certification_types 改名为 date_dict_certificate(保留原数据,随后用 dict_seed.sql 覆盖为官方枚举)
RENAME TABLE `certification_types` TO `date_dict_certificate`;
ALTER TABLE `date_dict_certificate`
ADD COLUMN `code` VARCHAR(50) NULL DEFAULT NULL COMMENT '证书类型枚举编码' AFTER `id`;
-- 4.2 国家地区编码字典
DROP TABLE IF EXISTS `date_dict_country`;
CREATE TABLE `date_dict_country` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` VARCHAR(50) NOT NULL COMMENT '国家/地区编码',
`name` VARCHAR(100) NOT NULL COMMENT '国家/地区中文名',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家地区编码字典';
-- 4.3 行政区划编码字典(省/市/区县三级,parent_code 关联)
DROP TABLE IF EXISTS `date_dict_area`;
CREATE TABLE `date_dict_area` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` VARCHAR(50) NOT NULL COMMENT '行政区划编码',
`name` VARCHAR(100) NOT NULL COMMENT '行政区划名称',
`parent_code` VARCHAR(50) DEFAULT NULL COMMENT '上级行政区划编码(省级为NULL)',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_parent` (`parent_code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='行政区划编码字典';
-- 4.4 招标行业分类字典(parent_code 预留大类/细分层级)
DROP TABLE IF EXISTS `date_dict_industry`;
CREATE TABLE `date_dict_industry` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` VARCHAR(50) NOT NULL COMMENT '行业编码',
`name` VARCHAR(100) NOT NULL COMMENT '行业名称',
`parent_code` VARCHAR(50) DEFAULT NULL COMMENT '上级行业编码(大类为NULL)',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_parent` (`parent_code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='招标行业分类字典';
-- 4.5 证书类型枚举字典
ALTER TABLE `date_dict_certificate`
MODIFY COLUMN `code` VARCHAR(50) NULL DEFAULT NULL COMMENT '证书类型枚举编码',
ADD UNIQUE KEY `uk_code` (`code`);
-- 4.6 来源渠道字典(source_channel 枚举,参考 certification_types 结构)
DROP TABLE IF EXISTS `source_channels`;
CREATE TABLE `source_channels` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` VARCHAR(50) NOT NULL COMMENT '渠道名称',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='来源渠道字典';
-- 来源渠道默认数据
INSERT INTO `source_channels` (`name`, `sort_order`) VALUES
('展会', 1), ('SNS/社交媒体', 2), ('竞价广告', 3), ('搜索引擎自然流量', 4),
('行业媒体/门户', 5), ('转介绍', 6), ('电话销售', 7), ('邮件营销', 8),
('地推', 9), ('代理商/渠道', 10), ('官网咨询', 11), ('其他', 99);
-- ------------------------------------------------------------
-- 字典数据(date_dict_country / date_dict_area / date_dict_industry / date_dict_certificate)
-- 请执行:sql/dict_seed.sql
-- ------------------------------------------------------------
-- ============================================================
-- 附:时间字段批量转换工具使用说明
-- 在部署机上执行:php tools/migrate_time_to_date.php
-- 该脚本会自动遍历 mysuperlink 库中所有 DATETIME/TIMESTAMP 字段并转换为 DATE,
-- 即满足"所有数据表时间字段调整为年-月-日"的需求,无需手工逐个核对。
-- ============================================================
+94 -11
View File
@@ -1,11 +1,15 @@
-- ============================================================ -- ============================================================
-- SuperLink Web 后台管理系统 - 新增系统表 -- SuperLink Web 后台管理系统 - 系统表(含 v1.0.16 数据字典表)
-- 数据库:mysuperlink (utf8mb4 / utf8mb4_unicode_ci) -- 数据库:mysuperlink (utf8mb4 / utf8mb4_unicode_ci)
-- 说明: -- 说明:
-- 1) 原库已有14张业务表(companies/persons/social_accounts/company_needs 等),本文件不改动它们。 -- 1) 原库已有14张业务表(companies/persons/social_accounts/company_needs 等),本文件不改动它们。
-- 2) 本文件新增文档要求的 3 张系统表:system_users / system_roles / system_logs(含默认数据)。 -- 2) 本文件定义系统表:system_users / system_roles / system_logs / channels,
-- 3) 另新增 1 张业务表 channels(渠道管理模块需要,原14张表中没有渠道表), -- 以及 v1.0.16 新增的数据字典表:date_dict_country / date_dict_area /
-- 如不需要渠道模块可直接跳过该段。 -- date_dict_industry / date_dict_certificate / source_channels。
-- 3) 按需求文档:所有时间字段调整为"年-月-日"(DATE 类型,不保留时分秒)。
-- 注意:DATE 默认值使用表达式 DEFAULT (CURRENT_DATE),需 MySQL 8.0.13+;
-- 若为 MySQL 5.7,请将时间字段保留为 DATETIME 或由应用层写入日期。
-- 4) 数据字典的完整数据由官方 Excel 生成,见 sql/dict_seed.sql(建表后执行)。
-- 执行方式:Navicat / phpMyAdmin / mysql 命令行执行本文件即可(可重复执行前请先删除旧表)。 -- 执行方式:Navicat / phpMyAdmin / mysql 命令行执行本文件即可(可重复执行前请先删除旧表)。
-- ============================================================ -- ============================================================
@@ -22,10 +26,10 @@ CREATE TABLE `system_users` (
`real_name` VARCHAR(50) DEFAULT NULL COMMENT '真实姓名', `real_name` VARCHAR(50) DEFAULT NULL COMMENT '真实姓名',
`role_id` INT UNSIGNED NOT NULL COMMENT '角色ID', `role_id` INT UNSIGNED NOT NULL COMMENT '角色ID',
`is_active` TINYINT(1) UNSIGNED DEFAULT 1 COMMENT '启用状态(0禁用,1启用)', `is_active` TINYINT(1) UNSIGNED DEFAULT 1 COMMENT '启用状态(0禁用,1启用)',
`last_login_time` DATETIME DEFAULT NULL COMMENT '最后登录时间', `last_login_time` DATE DEFAULT NULL COMMENT '最后登录时间(年-月-日)',
`last_login_ip` VARCHAR(50) DEFAULT NULL COMMENT '最后登录IP', `last_login_ip` VARCHAR(50) DEFAULT NULL COMMENT '最后登录IP',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `created_at` DATE DEFAULT (CURRENT_DATE) COMMENT '创建时间(年-月-日)',
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `updated_at` DATE DEFAULT (CURRENT_DATE) COMMENT '更新时间(年-月-日)',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`), UNIQUE KEY `uk_username` (`username`),
KEY `idx_role_id` (`role_id`) KEY `idx_role_id` (`role_id`)
@@ -40,7 +44,7 @@ CREATE TABLE `system_roles` (
`role_name` VARCHAR(50) NOT NULL COMMENT '角色名称', `role_name` VARCHAR(50) NOT NULL COMMENT '角色名称',
`permissions` JSON NOT NULL COMMENT '权限列表(菜单标识数组)', `permissions` JSON NOT NULL COMMENT '权限列表(菜单标识数组)',
`is_active` TINYINT(1) UNSIGNED DEFAULT 1 COMMENT '启用状态', `is_active` TINYINT(1) UNSIGNED DEFAULT 1 COMMENT '启用状态',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `created_at` DATE DEFAULT (CURRENT_DATE) COMMENT '创建时间(年-月-日)',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_name` (`role_name`) UNIQUE KEY `uk_role_name` (`role_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统角色表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统角色表';
@@ -59,7 +63,7 @@ CREATE TABLE `system_logs` (
`target_id` INT UNSIGNED DEFAULT NULL COMMENT '操作对象记录ID', `target_id` INT UNSIGNED DEFAULT NULL COMMENT '操作对象记录ID',
`content` JSON DEFAULT NULL COMMENT '操作内容(变更前后数据)', `content` JSON DEFAULT NULL COMMENT '操作内容(变更前后数据)',
`ip` VARCHAR(50) DEFAULT NULL COMMENT '客户端IP', `ip` VARCHAR(50) DEFAULT NULL COMMENT '客户端IP',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', `created_at` DATE DEFAULT (CURRENT_DATE) COMMENT '操作时间(年-月-日)',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`), KEY `idx_user_id` (`user_id`),
KEY `idx_action` (`action`), KEY `idx_action` (`action`),
@@ -97,9 +101,88 @@ CREATE TABLE `channels` (
`efficiency_score` DECIMAL(5,2) DEFAULT 0 COMMENT '效率评分(0-100)', `efficiency_score` DECIMAL(5,2) DEFAULT 0 COMMENT '效率评分(0-100)',
`remark` VARCHAR(255) DEFAULT NULL COMMENT '备注', `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
`is_active` TINYINT(1) UNSIGNED DEFAULT 1 COMMENT '启用状态', `is_active` TINYINT(1) UNSIGNED DEFAULT 1 COMMENT '启用状态',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `created_at` DATE DEFAULT (CURRENT_DATE) COMMENT '创建时间(年-月-日)',
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `updated_at` DATE DEFAULT (CURRENT_DATE) COMMENT '更新时间(年-月-日)',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_channel_type` (`channel_type`), KEY `idx_channel_type` (`channel_type`),
KEY `idx_is_active` (`is_active`) KEY `idx_is_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道管理表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道管理表';
-- ------------------------------------------------------------
-- 6. v1.0.16 数据字典表
-- ------------------------------------------------------------
-- 6.1 国家地区编码字典
DROP TABLE IF EXISTS `date_dict_country`;
CREATE TABLE `date_dict_country` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` VARCHAR(50) NOT NULL COMMENT '国家/地区编码',
`name` VARCHAR(100) NOT NULL COMMENT '国家/地区中文名',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家地区编码字典';
-- 6.2 行政区划编码字典(省/市/区县三级,parent_code 关联)
DROP TABLE IF EXISTS `date_dict_area`;
CREATE TABLE `date_dict_area` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` VARCHAR(50) NOT NULL COMMENT '行政区划编码',
`name` VARCHAR(100) NOT NULL COMMENT '行政区划名称',
`parent_code` VARCHAR(50) DEFAULT NULL COMMENT '上级行政区划编码(省级为NULL)',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_parent` (`parent_code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='行政区划编码字典';
-- 6.3 招标行业分类字典(parent_code 预留大类/细分层级)
DROP TABLE IF EXISTS `date_dict_industry`;
CREATE TABLE `date_dict_industry` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` VARCHAR(50) NOT NULL COMMENT '行业编码',
`name` VARCHAR(100) NOT NULL COMMENT '行业名称',
`parent_code` VARCHAR(50) DEFAULT NULL COMMENT '上级行业编码(大类为NULL)',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_parent` (`parent_code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='招标行业分类字典';
-- 6.4 证书类型枚举字典(原 certification_types 表改名而来;新装环境在此创建)
DROP TABLE IF EXISTS `date_dict_certificate`;
CREATE TABLE `date_dict_certificate` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`code` VARCHAR(50) DEFAULT NULL COMMENT '证书类型枚举编码',
`name` VARCHAR(100) NOT NULL COMMENT '证书类型名称',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='证书类型枚举字典';
-- 6.5 来源渠道字典(source_channel 枚举)
DROP TABLE IF EXISTS `source_channels`;
CREATE TABLE `source_channels` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` VARCHAR(50) NOT NULL COMMENT '渠道名称',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '启用状态',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='来源渠道字典';
-- 6.6 来源渠道默认数据
INSERT INTO `source_channels` (`name`, `sort_order`) VALUES
('展会', 1), ('SNS/社交媒体', 2), ('竞价广告', 3), ('搜索引擎自然流量', 4),
('行业媒体/门户', 5), ('转介绍', 6), ('电话销售', 7), ('邮件营销', 8),
('地推', 9), ('代理商/渠道', 10), ('官网咨询', 11), ('其他', 99);
-- 6.7 数据字典完整数据(国家/行政区划/行业/证书)由官方 Excel 生成:
-- 请执行 sql/dict_seed.sql
+55 -16
View File
@@ -88,21 +88,49 @@ $(function () {
/* ---------- 新增/编辑 ---------- */ /* ---------- 新增/编辑 ---------- */
$('#btn-add').on('click', function () { $('#btn-add').on('click', function () {
httpGet('company/cert_types.php').then(function (d) { httpGet('company/cert_types.php').then(function (d) {
openForm(null, [], [], d.types || []); loadDicts().then(function (dicts) {
openForm(null, [], [], d.types || [], dicts);
});
}); });
}); });
window.editCompany = function (id) { window.editCompany = function (id) {
httpGet('company/detail.php', { id: id }).then(function (d) { httpGet('company/detail.php', { id: id }).then(function (d) {
openForm(d.company, d.financials, d.certifications, d.certification_types); loadDicts().then(function (dicts) {
openForm(d.company, d.financials, d.certifications, d.certification_types, dicts);
});
}); });
}; };
function openForm(company, financials, certifications, certTypes) { /** 拉取数据字典(国家/行业/来源渠道),供表单下拉使用 */
function loadDicts() {
return httpGet('common/dicts.php', { type: 'all' }).catch(function () {
return { countries: [], industries: [], source_channels: [] };
});
}
/** 生成下拉选项:value 与当前值匹配则选中;若当前值不在字典中则追加一个选项保留原值 */
function dictOptions(list, cur, labelKey) {
var html = '<option value="">-</option>';
var found = false;
(list || []).forEach(function (item) {
var name = item[labelKey || 'name'];
var sel = (String(name) === String(cur)) ? ' selected' : '';
if (sel) found = true;
html += '<option value="' + escHtml(name) + '"' + sel + '>' + escHtml(name) + '</option>';
});
if (cur && !found) {
html += '<option value="' + escHtml(cur) + '" selected>' + escHtml(cur) + '(自定义)</option>';
}
return html;
}
function openForm(company, financials, certifications, certTypes, dicts) {
var isEdit = !!company; var isEdit = !!company;
var c = company || {}; var c = company || {};
financials = financials || []; financials = financials || [];
certifications = certifications || []; certifications = certifications || [];
certTypes = certTypes || []; certTypes = certTypes || [];
dicts = dicts || {};
// Tab1 工商信息 // Tab1 工商信息
var bizHtml = var bizHtml =
@@ -113,7 +141,7 @@ $(function () {
' <button type="button" class="btn btn-sm" id="btn-gongshang-query" title="预留:后续接入工商查询API,自动填写公司名称/注册号/注册地址等">工商查询</button>' + ' <button type="button" class="btn btn-sm" id="btn-gongshang-query" title="预留:后续接入工商查询API,自动填写公司名称/注册号/注册地址等">工商查询</button>' +
' </div></div>' + ' </div></div>' +
' <div class="form-item"><label>英文名称</label><input type="text" name="name_en" value="' + escHtml(c.name_en) + '"></div>' + ' <div class="form-item"><label>英文名称</label><input type="text" name="name_en" value="' + escHtml(c.name_en) + '"></div>' +
' <div class="form-item"><label>国家/地区</label><input type="text" name="country" value="' + escHtml(c.country) + '"></div>' + ' <div class="form-item"><label>国家/地区</label><select name="country">' + dictOptions(dicts.countries, c.country) + '</select></div>' +
' <div class="form-item"><label>注册号</label><input type="text" name="registration_number" value="' + escHtml(c.registration_number) + '"></div>' + ' <div class="form-item"><label>注册号</label><input type="text" name="registration_number" value="' + escHtml(c.registration_number) + '"></div>' +
' <div class="form-item"><label>注册地址</label><input type="text" name="address" value="' + escHtml(c.address) + '"></div>' + ' <div class="form-item"><label>注册地址</label><input type="text" name="address" value="' + escHtml(c.address) + '"></div>' +
' <div class="form-item"><label>法律形式</label><input type="text" name="legal_form" value="' + escHtml(c.legal_form) + '"></div>' + ' <div class="form-item"><label>法律形式</label><input type="text" name="legal_form" value="' + escHtml(c.legal_form) + '"></div>' +
@@ -127,9 +155,11 @@ $(function () {
var indHtml = var indHtml =
'<div class="form-grid">' + '<div class="form-grid">' +
' <div class="form-item"><label>业务角色</label><select name="business_role">' + roleOptions(c.business_role) + '</select></div>' + ' <div class="form-item"><label>业务角色</label><select name="business_role">' + roleOptions(c.business_role) + '</select></div>' +
' <div class="form-item"><label>行业</label><input type="text" name="industry" value="' + escHtml(c.industry) + '"></div>' + ' <div class="form-item"><label>行业</label><select name="industry">' + dictOptions(dicts.industries, c.industry) + '</select></div>' +
' <div class="form-item"><label>行业细分</label><input type="text" name="industry_subdivision" value="' + escHtml(c.industry_subdivision) + '"></div>' + ' <div class="form-item"><label>行业细分</label><input type="text" name="industry_subdivision" value="' + escHtml(c.industry_subdivision) + '"></div>' +
' <div class="form-item"><label>官网</label><input type="text" name="website" value="' + escHtml(c.website) + '"></div>' + ' <div class="form-item"><label>官网</label><input type="text" name="website" value="' + escHtml(c.website) + '"></div>' +
' <div class="form-item"><label>来源渠道</label><select name="source_channel">' + dictOptions(dicts.source_channels, c.source_channel) + '</select></div>' +
' <div class="form-item"><label>来源详情</label><input type="text" name="source_detail" value="' + escHtml(c.source_detail) + '" placeholder="如:华南工博会"></div>' +
'</div>'; '</div>';
// Tab3 财务信息:顶部独立字段(是否上市/股票代码/最新员工/最新营收)+ 年度财务明细列表 // Tab3 财务信息:顶部独立字段(是否上市/股票代码/最新员工/最新营收)+ 年度财务明细列表
@@ -350,6 +380,24 @@ $(function () {
window.viewCompany = function (id) { window.viewCompany = function (id) {
httpGet('company/detail.php', { id: id }).then(function (d) { httpGet('company/detail.php', { id: id }).then(function (d) {
var c = d.company; var c = d.company;
// 联系方式列表(social_accounts 中属于该公司的记录,仅展示)
var accountsHtml = '';
(d.accounts || []).forEach(function (a) {
var tag = '';
if (a.is_defult) tag = ' <span class="tag">常用</span>';
else if (a.is_primary) tag = ' <span class="tag">默认</span>';
accountsHtml += '<tr>' +
'<td>' + escHtml(a.platform || '-') + '</td>' +
'<td>' + escHtml(a.account_id || '-') + '</td>' +
'<td>' + (a.profile_url ? '<a href="' + escHtml(a.profile_url) + '" target="_blank">打开</a>' : '-') + '</td>' +
'<td>' + escHtml(a.remark || '-') + '</td>' +
'<td>' + (a.is_active ? '<span class="tag tag-green">启用</span>' : '<span class="tag tag-gray">停用</span>') + tag + '</td>' +
'</tr>';
});
var accountsSection = accountsHtml
? '<table class="grid" style="font-size:12px;"><tr><th>平台</th><th>账号/号码</th><th>主页链接</th><th>备注</th><th>状态</th></tr>' + accountsHtml + '</table>'
: '<div style="font-size:13px;color:#b6bfcc;">暂无联系方式</div>';
var html = var html =
'<div style="padding:18px 22px;">' + '<div style="padding:18px 22px;">' +
' <h3 style="margin-bottom:14px;">' + escHtml(c.name_zh || c.name_en || '企业 #' + c.id) + '</h3>' + ' <h3 style="margin-bottom:14px;">' + escHtml(c.name_zh || c.name_en || '企业 #' + c.id) + '</h3>' +
@@ -362,9 +410,10 @@ $(function () {
detailRow('注册资本', c.registered_capital) + detailRow('成立日期', c.established_date) + detailRow('注册资本', c.registered_capital) + detailRow('成立日期', c.established_date) +
detailRow('员工数', c.latest_employee_count) + detailRow('年营收', c.latest_annual_revenue) + detailRow('员工数', c.latest_employee_count) + detailRow('年营收', c.latest_annual_revenue) +
detailRow('是否上市', c.is_listed ? '是' : '否') + detailRow('股票代码', c.stock_code) + detailRow('是否上市', c.is_listed ? '是' : '否') + detailRow('股票代码', c.stock_code) +
detailRow('来源渠道', c.source_channel) + detailRow('来源详情', c.source_detail) +
'</div>' + '</div>' +
' <h4 style="margin:16px 0 8px;">经营范围</h4><div style="font-size:13px;color:#5a6472;">' + escHtml(c.business_scope || '-') + '</div>' + ' <h4 style="margin:16px 0 8px;">经营范围</h4><div style="font-size:13px;color:#5a6472;">' + escHtml(c.business_scope || '-') + '</div>' +
' <h4 style="margin:16px 0 8px;">关联需求</h4>' + miniTable(['联系人', '需求大类', '意向品类', '场景'], d.needs.map(function (n) { return [n.contact_person || '-', n.need_category || '-', n.target_product_category || '-', n.application_scenario || '-']; })) + ' <h4 style="margin:16px 0 8px;">联系方式</h4>' + accountsSection +
'</div>'; '</div>';
Dialog.open({ title: '企业详情', area: ['820px', 'auto'], content: html, btn: ['关闭'] }); Dialog.open({ title: '企业详情', area: ['820px', 'auto'], content: html, btn: ['关闭'] });
}); });
@@ -374,16 +423,6 @@ $(function () {
return '<div><b>' + label + ':</b>' + escHtml(val || '-') + '</div>'; return '<div><b>' + label + ':</b>' + escHtml(val || '-') + '</div>';
} }
function miniTable(headers, rows) {
if (!rows.length) return '<div style="font-size:13px;color:#b6bfcc;">暂无数据</div>';
var html = '<table class="grid" style="font-size:12px;">';
html += '<tr>' + headers.map(function (h) { return '<th>' + h + '</th>'; }).join('') + '</tr>';
rows.forEach(function (r) {
html += '<tr>' + r.map(function (v) { return '<td>' + escHtml(v) + '</td>'; }).join('') + '</tr>';
});
return html + '</table>';
}
/* ---------- 员工列表弹窗(按公司查看员工,分页 + 姓名/职级筛选) ---------- */ /* ---------- 员工列表弹窗(按公司查看员工,分页 + 姓名/职级筛选) ---------- */
var empCompanyId = 0; var empCompanyId = 0;
var empPage = 1; var empPage = 1;
+1 -1
View File
@@ -5,7 +5,7 @@ var BASE_URL = '/api/';
var PAGE_SIZE = 20; var PAGE_SIZE = 20;
/** 系统版本号(logo旁展示):修改代码后运行 tools/bump_version.php 自动递增 */ /** 系统版本号(logo旁展示):修改代码后运行 tools/bump_version.php 自动递增 */
var APP_VERSION = 'v1.0.15'; var APP_VERSION = 'v1.0.17';
/** 页脚版权/备案信息(在 config.js 中修改) */ /** 页脚版权/备案信息(在 config.js 中修改) */
var FOOTER_TEXT = '© 2026 SuperLink 管理系统 版权所有 | 备案号:请替换为真实备案号'; var FOOTER_TEXT = '© 2026 SuperLink 管理系统 版权所有 | 备案号:请替换为真实备案号';
+26 -2
View File
@@ -118,9 +118,14 @@ $(function () {
' <option value="company"' + (m.owner_type === 'company' ? ' selected' : '') + '>企业</option></select></div>' + ' <option value="company"' + (m.owner_type === 'company' ? ' selected' : '') + '>企业</option></select></div>' +
' <div class="form-item"><label>归属ID<span class="req">*</span></label><input type="number" name="owner_id" value="' + escHtml(m.owner_id) + '" ' + (isEdit ? 'disabled' : '') + '></div>' + ' <div class="form-item"><label>归属ID<span class="req">*</span></label><input type="number" name="owner_id" value="' + escHtml(m.owner_id) + '" ' + (isEdit ? 'disabled' : '') + '></div>' +
' <div class="form-item"><label>平台<span class="req">*</span></label>' + ' <div class="form-item"><label>平台<span class="req">*</span></label>' +
' <select name="platform">' + platformOpts(m.platform) + '</select></div>' + ' <select name="platform" id="media-platform">' + platformOpts(m.platform) + '</select></div>' +
' <div class="form-item"><label>账号/号码<span class="req">*</span></label><input type="text" name="account_id" value="' + escHtml(m.account_id) + '"></div>' + ' <div class="form-item"><label>账号/号码<span class="req">*</span></label>' +
' <div><input type="text" name="account_id" id="media-account" value="' + escHtml(m.account_id) + '">' +
' <div class="geo-hint" id="media-account-geo" style="font-size:12px;color:#2f8f46;margin-top:4px;display:none;"></div></div></div>' +
' <div class="form-item"><label>主页链接</label><input type="text" name="profile_url" value="' + escHtml(m.profile_url) + '"></div>' + ' <div class="form-item"><label>主页链接</label><input type="text" name="profile_url" value="' + escHtml(m.profile_url) + '"></div>' +
' <div class="form-item"><label>是否常用</label><select name="is_defult">' +
' <option value="0"' + (!m.is_defult ? ' selected' : '') + '>备用</option>' +
' <option value="1"' + (m.is_defult ? ' selected' : '') + '>常用</option></select></div>' +
' <div class="form-item"><label>默认联系方式</label><select name="is_primary">' + ' <div class="form-item"><label>默认联系方式</label><select name="is_primary">' +
' <option value="0"' + (!m.is_primary ? ' selected' : '') + '>否</option>' + ' <option value="0"' + (!m.is_primary ? ' selected' : '') + '>否</option>' +
' <option value="1"' + (m.is_primary ? ' selected' : '') + '>是</option></select></div>' + ' <option value="1"' + (m.is_primary ? ' selected' : '') + '>是</option></select></div>' +
@@ -144,6 +149,25 @@ $(function () {
$('#form-cancel').on('click', function () { Dialog.close(idx); }); $('#form-cancel').on('click', function () { Dialog.close(idx); });
// 手机号归属地自动识别(平台为 phone 时,录入号码自动识别归属地)
var $geo = $('#media-account-geo');
var bindGeo = function () {
if ($('#media-platform').val() !== 'phone') { $geo.text('').hide(); return; }
$('#media-account').off('blur.geo').on('blur.geo', function () {
var val = $.trim($(this).val());
if (!val) { $geo.text('').hide(); return; }
httpGet('common/phone_geo.php', { phone: val }).then(function (d) {
if (d && d.label && d.label !== '未知') {
$geo.text('归属地:' + d.label).show();
} else {
$geo.text('归属地:未能识别').show();
}
}).catch(function () { $geo.text('').hide(); });
});
};
$('#media-platform').on('change', bindGeo);
bindGeo();
$('#media-form').on('submit', function (e) { $('#media-form').on('submit', function (e) {
e.preventDefault(); e.preventDefault();
var data = {}; var data = {};
+197 -107
View File
@@ -30,7 +30,7 @@ $(function () {
' <button class="btn" id="btn-export">导出</button>' + ' <button class="btn" id="btn-export">导出</button>' +
' </div>' + ' </div>' +
' <div class="table-wrap"><table class="grid">' + ' <div class="table-wrap"><table class="grid">' +
' <thead><tr><th><input type="checkbox" id="check-all"></th><th>ID</th><th>姓名</th><th>性别</th><th>国籍</th><th>工作所在地</th><th>家乡</th><th>学历</th><th>联系方式</th><th>创建时间</th><th>操作</th></tr></thead>' + ' <thead><tr><th><input type="checkbox" id="check-all"></th><th>ID</th><th>姓名</th><th>性别</th><th>国籍</th><th>工作所在地</th><th>家乡</th><th>学历</th><th>手机</th><th>邮箱</th><th>创建时间</th><th>操作</th></tr></thead>' +
' <tbody id="person-tbody"></tbody>' + ' <tbody id="person-tbody"></tbody>' +
' </table></div>' + ' </table></div>' +
' <div class="pagination" id="pagination"></div>' + ' <div class="pagination" id="pagination"></div>' +
@@ -58,17 +58,17 @@ $(function () {
'<td>' + escHtml(r.work_location || '-') + '</td>' + '<td>' + escHtml(r.work_location || '-') + '</td>' +
'<td>' + escHtml(r.hometown || '-') + '</td>' + '<td>' + escHtml(r.hometown || '-') + '</td>' +
'<td>' + escHtml(r.education || '-') + '</td>' + '<td>' + escHtml(r.education || '-') + '</td>' +
'<td title="' + escHtml(r.contacts || '') + '">' + escHtml((r.contacts || '-').substring(0, 40)) + '</td>' + '<td>' + escHtml(r.phone || '-') + '</td>' +
'<td title="' + escHtml(r.email || '') + '">' + escHtml((r.email || '-').substring(0, 30)) + '</td>' +
'<td>' + fmtDate(r.created_at) + '</td>' + '<td>' + fmtDate(r.created_at) + '</td>' +
'<td class="ops">' + '<td class="ops">' +
' <a onclick="viewPerson(' + r.id + ')">详情</a>' + ' <a onclick="viewPerson(' + r.id + ')">详情</a>' +
' <a onclick="viewCompanies(' + r.id + ')">公司</a>' + ' <a onclick="viewResume(' + r.id + ')">履历</a>' +
' <a onclick="viewPersonProducts(' + r.id + ')">产品</a>' +
' <a onclick="editPerson(' + r.id + ')">编辑</a>' + ' <a onclick="editPerson(' + r.id + ')">编辑</a>' +
'</td></tr>'; '</td></tr>';
}); });
if (!rows.length) { if (!rows.length) {
html = '<tr><td colspan="11" class="empty-tip" style="padding:40px 0;">暂无数据</td></tr>'; html = '<tr><td colspan="12" class="empty-tip" style="padding:40px 0;">暂无数据</td></tr>';
} }
$('#person-tbody').html(html); $('#person-tbody').html(html);
renderPagination($('#pagination'), data, loadList); renderPagination($('#pagination'), data, loadList);
@@ -81,43 +81,155 @@ $(function () {
loadList(1); loadList(1);
}); });
$('#btn-add').on('click', function () { openForm(null); }); $('#btn-add').on('click', function () { openForm(null, null, null, null, null); });
window.editPerson = function (id) { window.editPerson = function (id) {
httpGet('person/detail.php', { id: id }).then(function (d) { httpGet('person/detail.php', { id: id }).then(function (d) {
openForm(d.person, d.accounts); openForm(d.person, d.accounts, d.phone, d.email, d.experiences);
}); });
}; };
function contactsInputHtml(accounts) { /** 手机号输入后自动识别归属地(录入手机号码时自动识别:国内-国家省-市;港澳台-中国香港/澳门/台湾;海外-国籍) */
accounts = accounts || []; function bindPhoneGeo($phoneInput, $geoHint) {
var html = '<div id="contacts-box">'; var timer = null;
if (!accounts.length) { $phoneInput.on('blur', function () {
accounts = [{ platform: 'email', account_id: '', is_primary: 1 }]; var val = $.trim($(this).val());
if (!val) { $geoHint.text('').hide(); return; }
clearTimeout(timer);
timer = setTimeout(function () {
httpGet('common/phone_geo.php', { phone: val }).then(function (d) {
if (d && d.label && d.label !== '未知') {
$geoHint.text('归属地:' + d.label).show();
} else {
$geoHint.text('归属地:未能识别').show();
}
}).catch(function () { $geoHint.text('').hide(); });
}, 300);
});
}
/** 省市区三级联动下拉(数据来自 date_dict_area 字典)
* value 格式:"省-市-区县"(直辖市省略市,如 "北京市-东城区") */
function areaSelectsHtml(target, value) {
return '<div class="area-cascade" data-target="' + target + '" style="display:flex;gap:6px;">' +
'<select class="area-prov" style="flex:1;min-width:0;"><option value="">省</option></select>' +
'<select class="area-city" style="flex:1;min-width:0;"><option value="">市</option></select>' +
'<select class="area-dist" style="flex:1;min-width:0;"><option value="">区/县</option></select>' +
'</div>' +
'<input type="hidden" name="' + target + '" value="' + escHtml(value || '') + '">';
}
/** 构建省/市/区三级联动:填充选项并按已有值回选 */
function initAreaCascade(areas) {
var provMap = {}; // code -> name
var childMap = {}; // parent_code -> [{code,name}]
(areas || []).forEach(function (a) {
if (!a.parent_code) {
provMap[a.code] = a.name;
} else {
(childMap[a.parent_code] = childMap[a.parent_code] || []).push({ code: a.code, name: a.name });
}
});
var provCodes = Object.keys(provMap);
function fillProv($box) {
var html = '<option value="">省</option>';
provCodes.forEach(function (code) {
html += '<option value="' + code + '">' + escHtml(provMap[code]) + '</option>';
});
$box.find('.area-prov').html(html);
} }
accounts.forEach(function (a, i) { function fillCity($box, provCode) {
html += '<div class="contact-row" style="display:flex;gap:6px;margin-bottom:6px;">' + var html = '<option value="">市</option>';
'<select name="c-platform" style="width:110px;height:32px;border:1px solid #d9dee8;border-radius:4px;padding:0 6px;">' + (childMap[provCode] || []).forEach(function (c) {
platformOpts(a.platform) + '</select>' + html += '<option value="' + c.code + '">' + escHtml(c.name) + '</option>';
'<input type="text" name="c-account" placeholder="账号/号码" value="' + escHtml(a.account_id) + '" style="flex:1;height:32px;border:1px solid #d9dee8;border-radius:4px;padding:0 8px;">' + });
'<label style="line-height:32px;font-size:12px;"><input type="checkbox" name="c-primary" ' + (a.is_primary ? 'checked' : '') + '> 默认</label>' + $box.find('.area-city').html(html);
'<button type="button" class="btn btn-sm" onclick="$(this).parent().remove()">删除</button></div>'; $box.find('.area-dist').html('<option value="">区/县</option>');
}
function fillDist($box, cityCode) {
var html = '<option value="">区/县</option>';
(childMap[cityCode] || []).forEach(function (c) {
html += '<option value="' + c.code + '">' + escHtml(c.name) + '</option>';
});
$box.find('.area-dist').html(html);
}
function combine($box) {
var target = $box.data('target');
var provName = $box.find('.area-prov option:selected').text();
var cityName = $box.find('.area-city option:selected').text();
var distName = $box.find('.area-dist option:selected').text();
var parts = [];
if ($box.find('.area-prov').val()) parts.push(provName);
if ($box.find('.area-city').val() && cityName !== provName) parts.push(cityName);
if ($box.find('.area-dist').val()) parts.push(distName);
$box.closest('.form-item').find('input[name="' + target + '"]').val(parts.join('-'));
}
$('.area-cascade').each(function () {
var $box = $(this);
var target = $box.data('target');
var current = $box.closest('.form-item').find('input[name="' + target + '"]').val() || '';
var parts = current.split('-');
var provName = parts[0] || '';
var cityName = parts[1] || '';
var distName = parts[2] || '';
fillProv($box);
// 回选省
var provCode = '';
provCodes.forEach(function (code) {
if (provMap[code] === provName) {
provCode = code;
$box.find('.area-prov').val(code);
}
});
if (!provCode) return;
fillCity($box, provCode);
var cityList = childMap[provCode] || [];
var cityCode = '';
// 直接按市名匹配;直辖市(省/市同名,如"北京市-东城区")时 parts[1] 实为区县
var direct = cityList.filter(function (c) { return c.name === cityName; });
if (direct.length) {
cityCode = direct[0].code;
$box.find('.area-city').val(cityCode);
} else if (cityList.length === 1 && cityList[0].name === provName) {
cityCode = cityList[0].code;
$box.find('.area-city').val(cityCode);
if (parts[1] && !distName) {
distName = parts[1]; // 直辖市:第二段实为区县
}
}
if (!cityCode) return;
fillDist($box, cityCode);
(childMap[cityCode] || []).forEach(function (c) {
if (c.name === distName) $box.find('.area-dist').val(c.code);
});
});
// 联动事件
$('.area-cascade').off('change.area').on('change.area', '.area-prov', function () {
var $box = $(this).closest('.area-cascade');
fillCity($box, $(this).val());
combine($box);
});
$('.area-cascade').off('change.areaCity').on('change.areaCity', '.area-city', function () {
var $box = $(this).closest('.area-cascade');
fillDist($box, $(this).val());
combine($box);
});
$('.area-cascade').off('change.areaDist').on('change.areaDist', '.area-dist', function () {
combine($(this).closest('.area-cascade'));
}); });
return html + '</div>' +
'<button type="button" class="btn btn-sm" id="add-contact">+ 添加联系方式</button>';
} }
function platformOpts(sel) { function openForm(person, accounts, phone, email, experiences) {
var list = ['phone', 'email', 'wechat', 'douyin', 'linkedin', 'x', 'facebook', 'instagram', 'tiktok', 'other'];
var html = '';
list.forEach(function (p) {
html += '<option value="' + p + '"' + (sel === p ? ' selected' : '') + '>' + p + '</option>';
});
return html;
}
function openForm(person, accounts) {
var isEdit = !!person; var isEdit = !!person;
var p = person || {}; var p = person || {};
var pPhone = phone || '';
var pEmail = email || '';
var content = var content =
'<form id="person-form" style="padding:18px 22px 4px;">' + '<form id="person-form" style="padding:18px 22px 4px;">' +
'<div class="form-grid">' + '<div class="form-grid">' +
@@ -127,14 +239,20 @@ $(function () {
' <option value="男"' + (p.gender === '男' ? ' selected' : '') + '>男</option>' + ' <option value="男"' + (p.gender === '男' ? ' selected' : '') + '>男</option>' +
' <option value="女"' + (p.gender === '女' ? ' selected' : '') + '>女</option>' + ' <option value="女"' + (p.gender === '女' ? ' selected' : '') + '>女</option>' +
' <option value="其他"' + (p.gender === '其他' ? ' selected' : '') + '>其他</option></select></div>' + ' <option value="其他"' + (p.gender === '其他' ? ' selected' : '') + '>其他</option></select></div>' +
' <div class="form-item"><label>国籍</label><input type="text" name="nationality" value="' + escHtml(p.nationality) + '"></div>' + ' <div class="form-item"><label>国籍</label><input type="text" name="nationality" value="' + escHtml(p.nationality) + '" list="person-country-list" placeholder="选择或输入">' +
' <datalist id="person-country-list"></datalist></div>' +
' <div class="form-item"><label>证件类型</label><input type="text" name="id_type" value="' + escHtml(p.id_type) + '"></div>' + ' <div class="form-item"><label>证件类型</label><input type="text" name="id_type" value="' + escHtml(p.id_type) + '"></div>' +
' <div class="form-item"><label>证件号码</label><input type="text" name="id_number" value="' + escHtml(p.id_number) + '"></div>' + ' <div class="form-item"><label>证件号码</label><input type="text" name="id_number" value="' + escHtml(p.id_number) + '"></div>' +
' <div class="form-item"><label>学历</label><input type="text" name="education" value="' + escHtml(p.education) + '"></div>' + ' <div class="form-item"><label>学历</label><input type="text" name="education" value="' + escHtml(p.education) + '"></div>' +
' <div class="form-item"><label>毕业院校</label><input type="text" name="graduated_from" value="' + escHtml(p.graduated_from) + '"></div>' + ' <div class="form-item"><label>毕业院校</label><input type="text" name="graduated_from" value="' + escHtml(p.graduated_from) + '"></div>' +
' <div class="form-item"><label>家乡</label><input type="text" name="hometown" value="' + escHtml(p.hometown) + '"></div>' + ' <div class="form-item"><label>家乡</label>' + areaSelectsHtml('hometown', p.hometown) + '</div>' +
' <div class="form-item"><label>工作所在地</label><input type="text" name="work_location" value="' + escHtml(p.work_location) + '"></div>' + ' <div class="form-item"><label>工作所在地</label>' + areaSelectsHtml('work_location', p.work_location) + '</div>' +
' <div class="form-item full"><label>联系方式</label>' + contactsInputHtml(accounts) + '</div>' + ' <div class="form-item"><label>手机</label>' +
' <div><input type="text" name="phone" id="person-phone" value="' + escHtml(pPhone) + '" placeholder="常用手机号">' +
' <div class="geo-hint" id="person-phone-geo" style="font-size:12px;color:#2f8f46;margin-top:4px;display:none;"></div></div></div>' +
' <div class="form-item"><label>邮箱</label><input type="text" name="email" value="' + escHtml(pEmail) + '" placeholder="常用邮箱"></div>' +
' <div class="form-item"><label>来源渠道</label><select name="source_channel" id="person-source-channel"><option value="">-</option></select></div>' +
' <div class="form-item"><label>来源详情</label><input type="text" name="source_detail" value="' + escHtml(p.source_detail) + '" placeholder="如:华南工博会"></div>' +
'</div>' + '</div>' +
'<div class="dialog-footer"><button type="button" class="btn" id="form-cancel">取消</button>' + '<div class="dialog-footer"><button type="button" class="btn" id="form-cancel">取消</button>' +
'<button type="submit" class="btn btn-primary">保存</button></div>' + '<button type="submit" class="btn btn-primary">保存</button></div>' +
@@ -147,16 +265,32 @@ $(function () {
btn: false btn: false
}); });
// 手机号归属地自动识别
bindPhoneGeo($('#person-phone'), $('#person-phone-geo'));
// 加载国籍字典 + 行政区划字典(三级联动) + 来源渠道字典
httpGet('common/dicts.php', { type: 'all' }).then(function (d) {
var $dl = $('#person-country-list');
var html = '';
(d.countries || []).forEach(function (c) { html += '<option value="' + escHtml(c.name) + '"></option>'; });
$dl.html(html);
// 省市区三级联动
initAreaCascade(d.areas || []);
var $sc = $('#person-source-channel');
var scHtml = '<option value="">-</option>';
var hasCur = false;
(d.source_channels || []).forEach(function (s) {
var sel = (s.name === p.source_channel) ? ' selected' : '';
if (sel) hasCur = true;
scHtml += '<option value="' + escHtml(s.name) + '"' + sel + '>' + escHtml(s.name) + '</option>';
});
if (p.source_channel && !hasCur) {
scHtml += '<option value="' + escHtml(p.source_channel) + '" selected>' + escHtml(p.source_channel) + '(自定义)</option>';
}
$sc.html(scHtml);
}).catch(function () {});
$('#form-cancel').on('click', function () { Dialog.close(idx); }); $('#form-cancel').on('click', function () { Dialog.close(idx); });
$('#add-contact').on('click', function () {
$('#contacts-box').append(
'<div class="contact-row" style="display:flex;gap:6px;margin-bottom:6px;">' +
'<select name="c-platform" style="width:110px;height:32px;border:1px solid #d9dee8;border-radius:4px;padding:0 6px;">' + platformOpts('') + '</select>' +
'<input type="text" name="c-account" placeholder="账号/号码" style="flex:1;height:32px;border:1px solid #d9dee8;border-radius:4px;padding:0 8px;">' +
'<label style="line-height:32px;font-size:12px;"><input type="checkbox" name="c-primary"> 默认</label>' +
'<button type="button" class="btn btn-sm" onclick="$(this).parent().remove()">删除</button></div>'
);
});
$('#person-form').on('submit', function (e) { $('#person-form').on('submit', function (e) {
e.preventDefault(); e.preventDefault();
@@ -164,27 +298,21 @@ $(function () {
$(this).find('input, select').each(function () { $(this).find('input, select').each(function () {
var name = $(this).attr('name'); var name = $(this).attr('name');
if (!name) return; if (!name) return;
if (name.indexOf('c-') === 0) return; data[name] = $(this).val();
if ($(this).attr('type') === 'checkbox') {
data[name] = $(this).prop('checked') ? 1 : 0;
} else {
data[name] = $(this).val();
}
}); });
if (!data.full_name) { Dialog.error('姓名为必填项'); return; } if (!data.full_name) { Dialog.error('姓名为必填项'); return; }
// 联系方式 // 联系方式(手机/邮箱,均为常用)
var contacts = []; var contacts = [];
$('#contacts-box .contact-row').each(function () { if ($.trim(data.phone || '')) {
var $row = $(this); contacts.push({ platform: 'phone', account_id: $.trim(data.phone), is_primary: 1, is_defult: 1 });
var account = $.trim($row.find('input[name="c-account"]').val()); }
var platform = $row.find('select[name="c-platform"]').val(); if ($.trim(data.email || '')) {
var primary = $row.find('input[name="c-primary"]').prop('checked') ? 1 : 0; contacts.push({ platform: 'email', account_id: $.trim(data.email), is_primary: 1, is_defult: 1 });
if (account) { }
contacts.push({ platform: platform, account_id: account, is_primary: primary });
}
});
data.contacts = JSON.stringify(contacts); data.contacts = JSON.stringify(contacts);
delete data.phone;
delete data.email;
if (isEdit) data.id = p.id; if (isEdit) data.id = p.id;
httpPost(isEdit ? 'person/update.php' : 'person/add.php', data).then(function () { httpPost(isEdit ? 'person/update.php' : 'person/add.php', data).then(function () {
@@ -223,11 +351,6 @@ $(function () {
window.viewPerson = function (id) { window.viewPerson = function (id) {
httpGet('person/detail.php', { id: id }).then(function (d) { httpGet('person/detail.php', { id: id }).then(function (d) {
var p = d.person; var p = d.person;
var accountsHtml = '';
(d.accounts || []).forEach(function (a) {
accountsHtml += '<div style="font-size:13px;padding:3px 0;">' + escHtml(a.platform) + ':' + escHtml(a.account_id) +
(a.is_primary ? ' <span class="tag">默认</span>' : '') + '</div>';
});
var expHtml = ''; var expHtml = '';
(d.experiences || []).forEach(function (e) { (d.experiences || []).forEach(function (e) {
expHtml += '<tr><td>' + escHtml(e.display_name || '-') + '</td><td>' + escHtml(e.position || '-') + '</td>' + expHtml += '<tr><td>' + escHtml(e.display_name || '-') + '</td><td>' + escHtml(e.position || '-') + '</td>' +
@@ -245,10 +368,14 @@ $(function () {
' <div><b>毕业院校:</b>' + escHtml(p.graduated_from || '-') + '</div>' + ' <div><b>毕业院校:</b>' + escHtml(p.graduated_from || '-') + '</div>' +
' <div><b>家乡:</b>' + escHtml(p.hometown || '-') + '</div>' + ' <div><b>家乡:</b>' + escHtml(p.hometown || '-') + '</div>' +
' <div><b>工作所在地:</b>' + escHtml(p.work_location || '-') + '</div>' + ' <div><b>工作所在地:</b>' + escHtml(p.work_location || '-') + '</div>' +
' <div><b>来源渠道:</b>' + escHtml(p.source_channel || '-') + '</div>' +
' <div><b>来源详情:</b>' + escHtml(p.source_detail || '-') + '</div>' +
' <div><b>union_id:</b>' + escHtml(p.union_id) + '</div>' + ' <div><b>union_id:</b>' + escHtml(p.union_id) + '</div>' +
' </div>' + ' </div>' +
' <h4 style="margin:16px 0 8px;">联系方式</h4>' + (accountsHtml || '<div style="font-size:13px;color:#b6bfcc;">暂无</div>') + ' <h4 style="margin:16px 0 8px;">联系方式</h4>' +
' <h4 style="margin:16px 0 8px;">工作经历</h4>' + ' <div style="font-size:13px;"><b>手机:</b>' + escHtml(d.phone || '-') + '</div>' +
' <div style="font-size:13px;margin-top:4px;"><b>邮箱:</b>' + escHtml(d.email || '-') + '</div>' +
' <h4 style="margin:16px 0 8px;">工作履历</h4>' +
' <table class="grid" style="font-size:12px;"><tr><th>公司</th><th>职位</th><th>部门</th><th>级别</th><th>时间</th></tr>' + ' <table class="grid" style="font-size:12px;"><tr><th>公司</th><th>职位</th><th>部门</th><th>级别</th><th>时间</th></tr>' +
(expHtml || '<tr><td colspan="5" style="color:#b6bfcc;text-align:center;">暂无</td></tr>') + '</table>' + (expHtml || '<tr><td colspan="5" style="color:#b6bfcc;text-align:center;">暂无</td></tr>') + '</table>' +
'</div>'; '</div>';
@@ -256,10 +383,10 @@ $(function () {
}); });
} }
/* ---------- 任职公司弹窗(按人员查看任职公司,分页) ---------- */ /* ---------- 任职履历弹窗(按人员查看任职公司/履历,分页) ---------- */
var compPersonId = 0; var compPersonId = 0;
window.viewCompanies = function (personId) { window.viewResume = function (personId) {
compPersonId = personId; compPersonId = personId;
var content = var content =
'<div style="padding:14px 18px 6px;overflow:hidden;height:330px;">' + '<div style="padding:14px 18px 6px;overflow:hidden;height:330px;">' +
@@ -276,7 +403,7 @@ $(function () {
' </table></div>' + ' </table></div>' +
' <div class="pagination" id="comp-pagination"></div>' + ' <div class="pagination" id="comp-pagination"></div>' +
'</div>'; '</div>';
Dialog.open({ title: '任职公司', area: ['720px', 'auto'], content: content, btn: ['关闭'] }); Dialog.open({ title: '工作履历', area: ['720px', 'auto'], content: content, btn: ['关闭'] });
loadCompanies(1); loadCompanies(1);
$('#comp-search').on('click', function () { loadCompanies(1); }); $('#comp-search').on('click', function () { loadCompanies(1); });
@@ -314,41 +441,4 @@ $(function () {
renderPagination($('#comp-pagination'), data, loadCompanies); renderPagination($('#comp-pagination'), data, loadCompanies);
}); });
} }
/* ---------- 产品品类弹窗(按人员任职公司查看产品,分页) ---------- */
var ppPersonId = 0;
window.viewPersonProducts = function (personId) {
ppPersonId = personId;
var content =
'<div style="padding:14px 18px 6px;overflow:hidden;height:330px;">' +
' <div class="table-wrap"><table class="grid" style="font-size:13px;">' +
' <thead><tr><th>公司</th><th>品类名称</th><th>品类描述</th><th>是否核心</th></tr></thead>' +
' <tbody id="pp-tbody"></tbody>' +
' </table></div>' +
' <div class="pagination" id="pp-pagination"></div>' +
'</div>';
Dialog.open({ title: '产品品类', area: ['760px', 'auto'], content: content, btn: ['关闭'] });
loadPersonProducts(1);
};
function loadPersonProducts(page) {
httpGet('person/products.php', { person_id: ppPersonId, page: page, limit: 5 }).then(function (data) {
var rows = data.list || [];
var html = '';
rows.forEach(function (r) {
html += '<tr>' +
'<td>' + escHtml(r.company_name) + '</td>' +
'<td>' + escHtml(r.category_name) + '</td>' +
'<td>' + escHtml(r.category_description || '-') + '</td>' +
'<td>' + (r.is_core ? '<span class="tag">核心</span>' : '-') + '</td>' +
'</tr>';
});
if (!rows.length) {
html = '<tr><td colspan="4" class="empty-tip" style="padding:30px 0;">暂无产品品类</td></tr>';
}
$('#pp-tbody').html(html);
renderPagination($('#pp-pagination'), data, loadPersonProducts);
});
}
}); });
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
* 时间字段批量转换工具(v1.0.16)
* 需求:mysuperlink 数据库所有数据表的时间字段调整为"年-月-日"(DATETIME/TIMESTAMP -> DATE)。
* 用法:php tools/migrate_time_to_date.php [库名]
* 说明:
* - 自动遍历指定库(默认取 config/database.php 的 dbname)中所有 DATETIME/TIMESTAMP 字段,
* 生成并执行 ALTER TABLE ... MODIFY ... DATE。
* - 对带默认值 CURRENT_TIMESTAMP 的字段,改为 DEFAULT (CURRENT_DATE)(需 MySQL 8.0.13+);
* 若执行环境为 MySQL 5.7,请手动调整或保留 DATETIME。
* - 已存在 DATE 类型的字段不会重复处理;脚本幂等,可重复执行。
*/
require __DIR__ . '/../config/database.php';
$dbname = $argv[1] ?? $cfg['dbname'];
$dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=%s', $cfg['host'], $cfg['port'], $dbname, $cfg['charset']);
try {
$pdo = new PDO($dsn, $cfg['username'], $cfg['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (Exception $e) {
fwrite(STDERR, "[错误] 数据库连接失败:" . $e->getMessage() . "\n");
exit(1);
}
// 查出所有 DATETIME / TIMESTAMP 列
$stmt = $pdo->prepare(
"SELECT TABLE_NAME, COLUMN_NAME, IS_NULLABLE, COLUMN_DEFAULT, EXTRA
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ? AND DATA_TYPE IN ('datetime', 'timestamp')
ORDER BY TABLE_NAME, ORDINAL_POSITION"
);
$stmt->execute([$dbname]);
$cols = $stmt->fetchAll();
if (!$cols) {
echo "未发现需要转换的 DATETIME/TIMESTAMP 字段。\n";
exit(0);
}
$converted = 0;
foreach ($cols as $col) {
$table = $col['TABLE_NAME'];
$name = $col['COLUMN_NAME'];
$nullable = $col['IS_NULLABLE'] === 'YES' ? 'NULL' : 'NOT NULL';
// 原默认值为 CURRENT_TIMESTAMP / on update CURRENT_TIMESTAMP 的,改为 (CURRENT_DATE)
$def = 'NULL';
if (stripos((string)$col['COLUMN_DEFAULT'], 'CURRENT_TIMESTAMP') !== false) {
$def = '(CURRENT_DATE)';
}
$extra = '';
if (stripos((string)$col['EXTRA'], 'on update') !== false) {
$extra = ' ON UPDATE (CURRENT_DATE)';
}
$sql = "ALTER TABLE `$table` MODIFY COLUMN `$name` DATE $nullable DEFAULT $def$extra";
try {
$pdo->exec($sql);
echo "转换:$table.$name DATETIME -> DATE\n";
$converted++;
} catch (Exception $e) {
fwrite(STDERR, "[跳过] $table.$name:" . $e->getMessage() . "\n");
}
}
echo "完成:共转换 $converted / " . count($cols) . " 个时间字段。\n";