270 lines
11 KiB
PHP
270 lines
11 KiB
PHP
<?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', 'stock_code', 'website'],
|
||
'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' => []];
|
||
$totalAll = 0;
|
||
$filledAll = 0;
|
||
|
||
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++;
|
||
}
|
||
}
|
||
$rate = $total > 0 ? round($filled / $total * 100, 1) : 100;
|
||
$result[$layer] = [
|
||
'filled' => $filled,
|
||
'total' => $total,
|
||
'rate' => $rate,
|
||
'passed' => $rate >= COMPLETENESS_THRESHOLDS[$layer],
|
||
];
|
||
$totalAll += $total;
|
||
$filledAll += $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);
|
||
}
|