所属层;virtual 为关联虚拟字段) */ const COMPLETENESS_LAYERS = [ 'persons' => [ 'core' => ['full_name'], 'important' => ['work_location'], 'optional' => ['gender', 'nationality', 'education', 'graduated_from', 'hometown', 'id_type', 'id_number'], 'virtual' => ['phone' => 'core', 'email' => 'core'], // 关联 social_accounts.platform ], 'companies' => [ 'core' => ['display_name', 'name_zh', 'business_role', 'industry'], 'important' => ['address', 'country'], 'optional' => ['registration_number', 'legal_form', 'legal_representative', 'business_scope', 'established_date', 'registered_capital', 'industry_subdivision', 'latest_employee_count', 'latest_annual_revenue', 'is_listed', 'website'], 'conditional' => ['stock_code' => ['layer' => 'optional', 'when' => ['is_listed' => '1']]], // 股票代码仅在“是否上市=是”时参与完整度 'virtual' => [], ], 'social_accounts' => [ 'core' => ['platform', 'account_id'], 'important' => ['is_primary'], 'optional' => ['profile_url', 'remark'], 'virtual' => [], ], 'media' => [ 'core' => ['social_account_id', 'account_level'], 'important' => ['follower_count', 'content_categories'], 'optional' => ['avg_read_count', 'certification_type', 'special_requirements', 'media_remark'], 'virtual' => [], ], ]; /** 各层达标门槛(%) */ const COMPLETENESS_THRESHOLDS = ['core' => 100, 'important' => 60, 'optional' => 40]; /** 字段中文名(前端展示缺失字段用) */ 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' => '中文名', '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 { * layers: { core:{filled,total,rate,passed}, important:{...}, optional:{...} }, * overall: float, overall_passed: bool, * missing: { core:[], important:[], optional:[] }, * level: core|important|optional|complete * } */ function completenessCalc($table, $row) { $layers = COMPLETENESS_LAYERS[$table] ?? null; $default = [ 'layers' => ['core' => ['filled' => 0, 'total' => 0, 'rate' => 100, 'passed' => true]], 'overall' => 100, 'overall_passed' => true, 'missing' => ['core' => [], 'important' => [], 'optional' => []], 'level' => 'complete', ]; if (!$layers) { return $default; } $result = []; $missing = ['core' => [], 'important' => [], 'optional' => []]; foreach (['core', 'important', 'optional'] as $layer) { $total = count($layers[$layer]); $filled = 0; foreach ($layers[$layer] as $field) { if (completenessIsMissing($row[$field] ?? null)) { $missing[$layer][] = $field; } else { $filled++; } } // 虚拟字段(persons phone/email)按配置层计入 foreach (($layers['virtual'] ?? []) as $vf => $vLayer) { if ($vLayer !== $layer) { continue; } $total++; if (completenessIsMissing($row[$vf] ?? null)) { $missing[$layer][] = $vf; } else { $filled++; } } $result[$layer] = ['filled' => $filled, 'total' => $total, 'rate' => 0, 'passed' => false]; } // 条件字段:满足条件才计入完整度(如 stock_code 仅在 is_listed=1 时统计) foreach (($layers['conditional'] ?? []) as $cf => $cond) { $layer = $cond['layer'] ?? 'optional'; $match = true; foreach (($cond['when'] ?? []) as $wf => $wv) { if ((string)($row[$wf] ?? '') !== (string)$wv) { $match = false; break; } } if (!$match) { continue; } $result[$layer]['total']++; if (completenessIsMissing($row[$cf] ?? null)) { $missing[$layer][] = $cf; } else { $result[$layer]['filled']++; } } // 条件字段处理完后重算各层 rate/passed 与整体 $totalAll = 0; $filledAll = 0; foreach (['core', 'important', 'optional'] as $layer) { $rate = $result[$layer]['total'] > 0 ? round($result[$layer]['filled'] / $result[$layer]['total'] * 100, 1) : 100; $result[$layer]['rate'] = $rate; $result[$layer]['passed'] = $rate >= COMPLETENESS_THRESHOLDS[$layer]; $totalAll += $result[$layer]['total']; $filledAll += $result[$layer]['filled']; } $overall = $totalAll > 0 ? round($filledAll / $totalAll * 100, 1) : 100; $overallPassed = $result['core']['passed'] && $result['important']['passed'] && $result['optional']['passed']; $level = 'complete'; if (!$result['core']['passed']) { $level = 'core'; // 缺核心(红) } elseif (!$result['important']['passed']) { $level = 'important'; // 缺重要(橙) } elseif (!$result['optional']['passed']) { $level = 'optional'; // 待补充(蓝) } return [ 'layers' => $result, 'overall' => $overall, 'overall_passed' => $overallPassed, 'missing' => $missing, '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|null { row, layers, overall, overall_passed, missing, level, is_complete } */ 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, 'layers' => $calc['layers'], 'overall' => $calc['overall'], 'overall_passed' => $calc['overall_passed'], 'missing' => $calc['missing'], 'level' => $calc['level'], 'is_complete' => $calc['overall_passed'], ]; } /** * 更新某条记录的 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); }