Files
superlink/api/common/completeness.php

307 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 完整度判定引擎(碎片处理方案 v2,2026-08-09)
* 三层独立判定:任一楼层不达标 → is_incomplete = 1
* - 核心层 必须 100% 达标(不达标 = 缺核心,最高优先级)
* - 重要层 必须 ≥ 60% 达标(不达标 = 缺重要)
* - 非必要层 必须 ≥ 40% 达标(不达标 = 待补充)
* 每层完整度 = 该层已填字段数 ÷ 该层总字段数 × 100%
* 整体完整度 = 全部已填字段数 ÷ 全部字段数 × 100%(展示用)
* 说明:persons 的 phone/email 为虚拟字段(来自 social_accounts 关联记录)
*/
require_once __DIR__ . '/db.php';
/** 四表字段分层定义(key => 所属层;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' => [],
// 主页链接/备注仅对真实社媒账号(非 phone/email)计入非必要层;手机/邮箱账号无主页,不适用
'conditional' => [
'profile_url' => ['layer' => 'optional', 'when' => ['platform_not_in' => ['phone', 'email']]],
'remark' => ['layer' => 'optional', 'when' => ['platform_not_in' => ['phone', 'email']]],
],
'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) {
// 支持 _not_in 后缀:字段值不在列表中才满足条件
if (substr($wf, -7) === '_not_in') {
$realField = substr($wf, 0, -7);
if (in_array((string)($row[$realField] ?? ''), (array)$wv, true)) {
$match = false;
break;
}
} else {
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);
}