层级),threshold 为完整度阈值 */ const COMPLETENESS_LAYERS = [ 'persons' => [ 'core' => ['full_name'], 'important' => ['work_location'], 'supplementary' => ['gender', 'nationality', 'education', 'graduated_from', 'hometown', 'id_type', 'id_number'], 'virtual' => ['phone' => 'core', 'email' => 'important'], // 关联 social_accounts.platform 'threshold' => 65, ], 'companies' => [ 'core' => ['display_name', 'industry'], 'important' => ['address', 'business_role', 'country'], 'supplementary' => ['name_zh', 'name_en', 'registration_number', 'legal_form', 'legal_representative', 'business_scope', 'established_date', 'registered_capital', 'industry_subdivision', 'latest_employee_count', 'latest_annual_revenue', 'is_listed', 'stock_code', 'website'], 'virtual' => [], 'threshold' => 65, ], 'social_accounts' => [ 'core' => ['platform', 'account_id'], 'important' => ['is_primary'], 'supplementary' => ['profile_url', 'remark'], 'virtual' => [], 'threshold' => 60, ], 'media' => [ 'core' => ['social_account_id', 'account_level'], 'important' => ['follower_count', 'content_categories'], 'supplementary' => ['certification_type', 'avg_read_count', 'special_requirements', 'media_remark'], 'virtual' => [], 'threshold' => 55, ], ]; /** 字段中文名(前端展示缺失字段用) */ const COMPLETENESS_LABELS = [ 'full_name' => '姓名', 'phone' => '手机', 'email' => '邮箱', 'work_location' => '工作所在地', 'gender' => '性别', 'nationality' => '国籍', 'education' => '学历', 'graduated_from' => '毕业院校', 'hometown' => '家乡', 'id_type' => '证件类型', 'id_number' => '证件号码', 'display_name' => '名称', 'industry' => '行业', 'address' => '地址', 'business_role' => '业务类型', 'country' => '国家/地区', 'name_zh' => '中文名', 'name_en' => '英文名', 'registration_number' => '注册号', 'legal_form' => '企业类型', 'legal_representative' => '法定代表人', 'business_scope' => '经营范围', 'established_date' => '成立日期', 'registered_capital' => '注册资本', 'industry_subdivision' => '行业细分', 'latest_employee_count' => '员工数', 'latest_annual_revenue' => '年营收', 'is_listed' => '是否上市', 'stock_code' => '股票代码', 'website' => '官网', 'platform' => '平台', 'account_id' => '账号', 'is_primary' => '常用', 'profile_url' => '主页链接', 'remark' => '备注', 'social_account_id' => '关联账号', 'account_level' => '账号等级', 'follower_count' => '粉丝数', 'content_categories' => '内容领域', 'certification_type' => '认证类型', 'avg_read_count' => '平均阅读', 'special_requirements' => '特殊要求', 'media_remark' => '评估备注', ]; /** 字段是否缺失:null / 空串 / 纯空白 视为缺失(0 视为已填写) */ function completenessIsMissing($v) { return ($v === null) || (trim((string)$v) === ''); } /** * 根据分层定义计算完整度 * @param string $table persons|companies|social_accounts|media * @param array $row 该表记录(persons 需已注入虚拟字段 phone/email) * @return array { score:int, missing_core:[], missing_important:[], missing_supp:[], level:string } */ function completenessCalc($table, $row) { $layers = COMPLETENESS_LAYERS[$table] ?? null; if (!$layers) { return ['score' => 100, 'missing_core' => [], 'missing_important' => [], 'missing_supp' => [], 'level' => 'complete']; } $missing = ['core' => [], 'important' => [], 'supplementary' => []]; foreach (['core' => 'core', 'important' => 'important', 'supplementary' => 'supplementary'] as $layer => $bucket) { foreach ($layers[$layer] as $field) { if (completenessIsMissing($row[$field] ?? null)) { $missing[$bucket][] = $field; } } } // 虚拟字段(如人员 phone/email)按配置的层级计入 foreach (($layers['virtual'] ?? []) as $field => $layer) { if (completenessIsMissing($row[$field] ?? null)) { $missing[$layer][] = $field; } } $score = 100 - count($missing['core']) * 20 - count($missing['important']) * 8 - count($missing['supplementary']) * 2; $score = max(0, $score); $level = 'complete'; if (!empty($missing['core'])) { $level = 'core'; // 缺核心(红) } elseif (!empty($missing['important'])) { $level = 'important'; // 缺重要(橙) } elseif (!empty($missing['supplementary'])) { $level = 'supplementary'; // 待补充(蓝) } return [ 'score' => $score, 'missing_core' => $missing['core'], 'missing_important' => $missing['important'], 'missing_supp' => $missing['supplementary'], 'level' => $level, ]; } /** 人员记录:注入虚拟字段 phone/email(来自 social_accounts) */ function personWithVirtualFields(PDO $pdo, array $row) { $stmt = $pdo->prepare( "SELECT platform, account_id FROM social_accounts WHERE owner_type = 'person' AND owner_id = ? AND is_active = 1 AND platform IN ('phone','email') ORDER BY is_primary DESC, id ASC" ); $stmt->execute([$row['id']]); foreach ($stmt->fetchAll() as $acc) { if (!isset($row[$acc['platform']])) { $row[$acc['platform']] = $acc['account_id']; } } return $row; } /** * 计算某条记录的完整度信息 * @param PDO $pdo * @param string $table persons|companies|social_accounts|media * @param int $id * @return array { row, score, missing_core, missing_important, missing_supp, level, threshold } */ function completenessInfo(PDO $pdo, $table, $id) { $row = null; switch ($table) { case 'persons': $stmt = $pdo->prepare("SELECT * FROM persons WHERE id = ?"); $stmt->execute([$id]); $row = $stmt->fetch(); if ($row) { $row = personWithVirtualFields($pdo, $row); } break; case 'companies': $stmt = $pdo->prepare("SELECT * FROM companies WHERE id = ?"); $stmt->execute([$id]); $row = $stmt->fetch(); break; case 'social_accounts': $stmt = $pdo->prepare("SELECT * FROM social_accounts WHERE id = ?"); $stmt->execute([$id]); $row = $stmt->fetch(); break; case 'media': $stmt = $pdo->prepare("SELECT * FROM media_commercial_attributes WHERE social_account_id = ?"); $stmt->execute([$id]); $row = $stmt->fetch(); if ($row) { $row['id'] = $row['social_account_id']; } break; } if (!$row) { return null; } $calc = completenessCalc($table, $row); return [ 'row' => $row, 'score' => $calc['score'], 'missing_core' => $calc['missing_core'], 'missing_important' => $calc['missing_important'], 'missing_supp' => $calc['missing_supp'], 'level' => $calc['level'], 'threshold' => COMPLETENESS_LAYERS[$table]['threshold'] ?? 65, 'is_complete' => $calc['score'] >= (COMPLETENESS_LAYERS[$table]['threshold'] ?? 65), ]; } /** * 更新某条记录的 is_incomplete 标记(完整度低于阈值 → 1) * @param PDO $pdo * @param string $table persons|companies|social_accounts|media * @param int $id * @return array|null completenessInfo 结果(记录不存在返回 null) */ function updateIncomplete(PDO $pdo, $table, $id) { $info = completenessInfo($pdo, $table, $id); if (!$info) { return null; } $flag = $info['is_complete'] ? 0 : 1; switch ($table) { case 'persons': $pdo->prepare("UPDATE persons SET is_incomplete = ? WHERE id = ?")->execute([$flag, $id]); break; case 'companies': $pdo->prepare("UPDATE companies SET is_incomplete = ? WHERE id = ?")->execute([$flag, $id]); break; case 'social_accounts': $pdo->prepare("UPDATE social_accounts SET is_incomplete = ? WHERE id = ?")->execute([$flag, $id]); break; case 'media': $pdo->prepare("UPDATE media_commercial_attributes SET is_incomplete = ? WHERE social_account_id = ?")->execute([$flag, $id]); break; } return $info; } /** 更新某个归属方(人员/企业)全部 social_accounts 的 is_incomplete */ function updateAccountsIncomplete(PDO $pdo, $ownerType, $ownerId) { $stmt = $pdo->prepare("SELECT id FROM social_accounts WHERE owner_type = ? AND owner_id = ?"); $stmt->execute([$ownerType, $ownerId]); foreach ($stmt->fetchAll() as $acc) { updateIncomplete($pdo, 'social_accounts', (int)$acc['id']); } } /** 更新媒体账号(social_accounts + 其商业属性)的 is_incomplete */ function updateMediaIncomplete(PDO $pdo, $socialAccountId) { updateIncomplete($pdo, 'social_accounts', $socialAccountId); updateIncomplete($pdo, 'media', $socialAccountId); }