v1.0.27: 碎片处理中心(完整方案) - 完整度计算引擎(四表分层/阈值)+is_incomplete标记(写操作挂钩)+api/fragment八接口(统计/硬碎片列表详情转换废弃/软碎片列表详情补全)+fragment.html前端(指标卡+硬软碎片双区块)

This commit is contained in:
nanguaboss
2026-08-09 15:47:05 +08:00
parent b68232bff3
commit 76431c20ac
25 changed files with 1342 additions and 3 deletions
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* 硬碎片公共逻辑:字段分层 / 完整度计算 / 内容摘要
* 被 hard_list.php 与 hard_detail.php 共用
*/
/** 硬碎片字段分层(按 source_type) */
const HARD_LAYERS = [
'company' => ['core' => ['company_name', 'contact_phone'], 'important' => ['contact_email'], 'supplementary' => ['person_name', 'description', 'target_product_category']],
'person' => ['core' => ['person_name', 'contact_phone'], 'important' => ['contact_email'], 'supplementary' => ['company_name', 'description', 'target_product_category']],
'product' => ['core' => ['company_name', 'target_product_category'], 'important' => ['contact_phone', 'contact_email'], 'supplementary' => ['person_name', 'description']],
'need' => ['core' => ['company_name', 'target_product_category'], 'important' => ['contact_phone', 'contact_email'], 'supplementary' => ['person_name', 'description']],
'mixed' => ['core' => ['company_name', 'person_name'], 'important' => ['contact_phone', 'contact_email'], 'supplementary' => ['description', 'target_product_category']],
];
const HARD_LABELS = [
'company_name' => '公司名称', 'person_name' => '联系人', 'contact_phone' => '电话',
'contact_email' => '邮箱', 'target_product_category' => '产品品类', 'description' => '描述',
];
/** 计算硬碎片完整度与缺失层级 */
function hardCompleteness($row)
{
$layers = HARD_LAYERS[$row['source_type']] ?? HARD_LAYERS['mixed'];
$missing = ['core' => [], 'important' => [], 'supplementary' => []];
foreach (['core', 'important', 'supplementary'] as $layer) {
foreach ($layers[$layer] as $field) {
if ($row[$field] === null || trim((string)$row[$field]) === '') {
$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, 'level' => $level, 'missing' => $missing];
}
/** 内容摘要:拼接已有关键信息 */
function hardSummary($row)
{
$parts = [];
if (!empty($row['company_name'])) $parts[] = '公司:' . $row['company_name'];
if (!empty($row['person_name'])) $parts[] = '联系人:' . $row['person_name'];
if (!empty($row['contact_phone'])) $parts[] = '手机:' . $row['contact_phone'];
if (!empty($row['contact_email'])) $parts[] = '邮箱:' . $row['contact_email'];
if (!empty($row['target_product_category'])) $parts[] = '品类:' . $row['target_product_category'];
if (empty($parts) && !empty($row['description'])) $parts[] = '描述:' . mb_substr($row['description'], 0, 20);
return implode(' ', $parts);
}
+163
View File
@@ -0,0 +1,163 @@
<?php
/**
* 硬碎片转换为主表接口 POST /api/fragment/hard_convert.php
* 入参:id(硬碎片ID,必填)/ target_type(company|person|product|need,必填)/
* 补全字段:company_name, person_name, contact_phone, contact_email, target_product_category,
* description, industry, need_category, application_scenario, contact_person
* 逻辑:按目标类型写入主表(product/need 缺公司时自动建公司)→ 标记碎片已转换(converted_type/id, processed_at)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/logger.php';
require_once __DIR__ . '/../common/completeness.php';
checkAjax();
checkPermission('preliminary');
$id = (int)($_POST['id'] ?? 0);
$targetType = trim($_POST['target_type'] ?? '');
if ($id <= 0 || !in_array($targetType, ['company', 'person', 'product', 'need'], true)) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT * FROM preliminary_data WHERE id = ? AND is_active = 1");
$stmt->execute([$id]);
$frag = $stmt->fetch();
if (!$frag) {
Response::error('碎片不存在');
}
if (in_array($frag['status'], ['已转换', '已废弃'], true)) {
Response::error('该碎片已处理(' . $frag['status'] . ')');
}
$companyName = trim($_POST['company_name'] ?? ($frag['company_name'] ?? ''));
$personName = trim($_POST['person_name'] ?? ($frag['person_name'] ?? ''));
$phone = trim($_POST['contact_phone'] ?? ($frag['contact_phone'] ?? ''));
$email = trim($_POST['contact_email'] ?? ($frag['contact_email'] ?? ''));
$category = trim($_POST['target_product_category'] ?? ($frag['target_product_category'] ?? ''));
$description = trim($_POST['description'] ?? ($frag['description'] ?? ''));
$industry = trim($_POST['industry'] ?? '');
$sourceChannel = $frag['source_channel'] ?? null;
$convertedId = 0;
/** 创建 social_accounts(phone/email,常用) */
$saveContacts = function ($ownerType, $ownerId) use ($pdo, $phone, $email) {
if ($phone !== '') {
$pdo->prepare(
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, is_primary, is_defult)
VALUES (?, ?, 'phone', ?, 1, 1)"
)->execute([$ownerType, $ownerId, $phone]);
}
if ($email !== '') {
$pdo->prepare(
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, is_primary, is_defult)
VALUES (?, ?, 'email', ?, 1, 1)"
)->execute([$ownerType, $ownerId, $email]);
}
};
/** 按名称找公司,找不到则自动创建 */
$resolveCompany = function () use ($pdo, $companyName, $sourceChannel, $industry) {
$stmt = $pdo->prepare("SELECT id FROM companies WHERE display_name = ? AND is_active = 1 LIMIT 1");
$stmt->execute([$companyName]);
$cid = (int)$stmt->fetchColumn();
if ($cid > 0) {
return $cid;
}
$pdo->prepare(
"INSERT INTO companies (display_name, name_zh, industry, source_channel) VALUES (?, ?, ?, ?)"
)->execute([$companyName, $companyName, $industry !== '' ? $industry : null, $sourceChannel]);
return (int)$pdo->lastInsertId();
};
switch ($targetType) {
case 'company':
if ($companyName === '') {
Response::error('公司名称为必填项', 400);
}
$pdo->prepare(
"INSERT INTO companies (display_name, name_zh, industry, business_scope, source_channel, source_detail)
VALUES (?, ?, ?, ?, ?, ?)"
)->execute([
$companyName, $companyName,
$industry !== '' ? $industry : null,
$description !== '' ? $description : null,
$sourceChannel,
$frag['description'] ?? null,
]);
$convertedId = (int)$pdo->lastInsertId();
$saveContacts('company', $convertedId);
updateIncomplete($pdo, 'companies', $convertedId);
updateAccountsIncomplete($pdo, 'company', $convertedId);
break;
case 'person':
if ($personName === '') {
Response::error('联系人为必填项', 400);
}
$unionId = 'P' . date('YmdHis') . substr(uniqid(), -6);
$pdo->prepare(
"INSERT INTO persons (union_id, full_name, source_channel, source_detail) VALUES (?, ?, ?, ?)"
)->execute([$unionId, $personName, $sourceChannel, $frag['description'] ?? null]);
$convertedId = (int)$pdo->lastInsertId();
$saveContacts('person', $convertedId);
updateIncomplete($pdo, 'persons', $convertedId);
updateAccountsIncomplete($pdo, 'person', $convertedId);
break;
case 'product':
if ($companyName === '') {
Response::error('公司名称为必填项', 400);
}
if ($category === '') {
Response::error('产品品类为必填项', 400);
}
$companyId = $resolveCompany();
$pdo->prepare(
"INSERT INTO company_products (company_id, category_name, category_description)
VALUES (?, ?, ?)"
)->execute([$companyId, $category, $description !== '' ? $description : null]);
$convertedId = (int)$pdo->lastInsertId();
updateIncomplete($pdo, 'companies', $companyId);
break;
case 'need':
if ($companyName === '') {
Response::error('公司名称为必填项', 400);
}
$companyId = $resolveCompany();
$contactPerson = trim($_POST['contact_person'] ?? ($frag['person_name'] ?? ''));
$needCategory = trim($_POST['need_category'] ?? '');
$scenario = trim($_POST['application_scenario'] ?? '');
$pdo->prepare(
"INSERT INTO company_needs (company_id, contact_person, need_category, target_product_category, application_scenario, description)
VALUES (?, ?, ?, ?, ?, ?)"
)->execute([
$companyId,
$contactPerson !== '' ? $contactPerson : null,
$needCategory !== '' ? $needCategory : null,
$category !== '' ? $category : null,
$scenario !== '' ? $scenario : null,
$description !== '' ? $description : null,
]);
$convertedId = (int)$pdo->lastInsertId();
updateIncomplete($pdo, 'companies', $companyId);
break;
}
// 标记碎片已转换
$pdo->prepare(
"UPDATE preliminary_data SET status = '已转换', converted_type = ?, converted_id = ?, processed_at = NOW() WHERE id = ?"
)->execute([$targetType, $convertedId, $id]);
logCurrent('convert', 'fragment', 'preliminary_data', $id, [
'target_type' => $targetType,
'converted_id' => $convertedId,
'fields' => ['company_name' => $companyName, 'person_name' => $personName, 'phone' => $phone, 'email' => $email],
]);
Response::success(['converted_type' => $targetType, 'converted_id' => $convertedId], '转换成功');
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* 硬碎片详情接口 GET /api/fragment/hard_detail.php?id=1
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/hard_common.php';
checkPermission('preliminary');
$id = (int)($_REQUEST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT * FROM preliminary_data WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch();
if (!$row) {
Response::error('碎片不存在');
}
$calc = hardCompleteness($row);
$row['completeness'] = $calc['score'];
$row['level'] = $calc['level'];
$row['missing'] = $calc['missing'];
$row['summary'] = hardSummary($row);
$row['source_type_label'] = ['company' => '企业', 'person' => '人员', 'product' => '产品', 'need' => '需求', 'mixed' => '混合'][$row['source_type']] ?? $row['source_type'];
Response::success($row);
+33
View File
@@ -0,0 +1,33 @@
<?php
/**
* 硬碎片废弃接口 POST /api/fragment/hard_discard.php
* 入参:id
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/logger.php';
checkAjax();
checkPermission('preliminary');
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT id, status FROM preliminary_data WHERE id = ? AND is_active = 1");
$stmt->execute([$id]);
$frag = $stmt->fetch();
if (!$frag) {
Response::error('碎片不存在');
}
if (in_array($frag['status'], ['已转换', '已废弃'], true)) {
Response::error('该碎片已处理(' . $frag['status'] . ')');
}
$pdo->prepare("UPDATE preliminary_data SET status = '已废弃', processed_at = NOW() WHERE id = ?")->execute([$id]);
logCurrent('discard', 'fragment', 'preliminary_data', $id, ['status' => '已废弃']);
Response::success(null, '已废弃');
+62
View File
@@ -0,0 +1,62 @@
<?php
/**
* 硬碎片列表接口 GET /api/fragment/hard_list.php
* 参数:page / limit / source_type / status / source_channel / keyword
* 返回:{ list, total, page, limit },每行含完整度 score、缺失层级、内容摘要
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/hard_common.php';
checkPermission('preliminary');
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
$status = trim($_REQUEST['status'] ?? '');
$sourceType = trim($_REQUEST['source_type'] ?? '');
$sourceChannel = trim($_REQUEST['source_channel'] ?? '');
$where = ["is_active = 1", "status NOT IN ('已转换','已废弃')"];
$params = [];
if ($keyword !== '') {
$where[] = '(company_name LIKE ? OR person_name LIKE ? OR contact_phone LIKE ? OR contact_email LIKE ? OR description LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like, $like, $like, $like);
}
if ($status !== '') { $where[] = 'status = ?'; $params[] = $status; }
if ($sourceType !== '') { $where[] = 'source_type = ?'; $params[] = $sourceType; }
if ($sourceChannel !== '') { $where[] = 'source_channel = ?'; $params[] = $sourceChannel; }
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM preliminary_data WHERE $whereSql");
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, source_type, company_name, person_name, contact_phone, contact_email,
target_product_category, description, status, source_channel, recorded_by,
follow_person, converted_type, converted_id, created_at, updated_at
FROM preliminary_data
WHERE $whereSql
ORDER BY id DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute($params);
$rows = $stmt->fetchAll();
$list = [];
foreach ($rows as $r) {
$calc = hardCompleteness($r);
$r['completeness'] = $calc['score'];
$r['level'] = $calc['level'];
$r['missing'] = $calc['missing'];
$r['summary'] = hardSummary($r);
$r['source_type_label'] = ['company' => '企业', 'person' => '人员', 'product' => '产品', 'need' => '需求', 'mixed' => '混合'][$r['source_type']] ?? $r['source_type'];
$list[] = $r;
}
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* 软碎片详情接口 GET /api/fragment/soft_detail.php?table=persons&id=1
* 返回:{ table, id, name, score, level, missing_core, missing_important, missing_supp, threshold, row }
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/completeness.php';
checkPermission('preliminary');
$table = trim($_REQUEST['table'] ?? '');
$id = (int)($_REQUEST['id'] ?? 0);
if (!in_array($table, ['persons', 'companies', 'social_accounts', 'media'], true) || $id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$info = completenessInfo($pdo, $table, $id);
if (!$info) {
Response::error('记录不存在');
}
$tableLabels = ['persons' => '人员', 'companies' => '企业', 'social_accounts' => '联系方式', 'media' => '媒体'];
$name = '';
switch ($table) {
case 'persons':
$name = $info['row']['full_name'] ?? '';
break;
case 'companies':
$name = $info['row']['display_name'] ?? '';
break;
case 'social_accounts':
$name = $info['row']['account_id'] ?? '';
break;
case 'media':
$acc = $pdo->prepare("SELECT account_id FROM social_accounts WHERE id = ?");
$acc->execute([$id]);
$name = (string)$acc->fetchColumn();
break;
}
Response::success([
'table' => $table,
'table_label' => $tableLabels[$table],
'id' => $id,
'name' => $name ?: ('#' . $id),
'score' => $info['score'],
'level' => $info['level'],
'threshold' => $info['threshold'],
'missing_core' => array_map(function ($f) { return ['key' => $f, 'label' => COMPLETENESS_LABELS[$f] ?? $f]; }, $info['missing_core']),
'missing_important' => array_map(function ($f) { return ['key' => $f, 'label' => COMPLETENESS_LABELS[$f] ?? $f]; }, $info['missing_important']),
'missing_supp' => array_map(function ($f) { return ['key' => $f, 'label' => COMPLETENESS_LABELS[$f] ?? $f]; }, $info['missing_supp']),
'row' => $info['row'],
]);
+83
View File
@@ -0,0 +1,83 @@
<?php
/**
* 软碎片列表接口 GET /api/fragment/soft_list.php
* 参数:page / limit / table(所属表 persons|companies|social_accounts|media)/ level(core|important|supplementary)/ keyword
* 返回:{ list, total, page, limit },每行含 完整度 score、缺失层级标签、名称、缺失字段、所属表、录入时间
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/completeness.php';
checkPermission('preliminary');
[$page, $limit] = pageParams();
$table = trim($_REQUEST['table'] ?? '');
$level = trim($_REQUEST['level'] ?? '');
$keyword = trim($_REQUEST['keyword'] ?? '');
$tableMap = [
'persons' => ['label' => '人员', 'name' => 'full_name'],
'companies' => ['label' => '企业', 'name' => 'display_name'],
'social_accounts' => ['label' => '联系方式', 'name' => 'account_id'],
'media' => ['label' => '媒体', 'name' => null], // media 需 JOIN social_accounts 取名
];
$pdo = DB::getInstance()->getPdo();
$all = [];
foreach ($tableMap as $t => $cfg) {
if ($table !== '' && $table !== $t) {
continue;
}
if ($t === 'media') {
$rows = $pdo->query(
"SELECT m.social_account_id AS id, sa.account_id AS name, m.updated_at AS created_at
FROM media_commercial_attributes m
LEFT JOIN social_accounts sa ON sa.id = m.social_account_id
WHERE m.is_incomplete = 1"
)->fetchAll();
} else {
$rows = $pdo->query("SELECT id, {$cfg['name']} AS name, created_at FROM `$t` WHERE is_incomplete = 1")->fetchAll();
}
foreach ($rows as $r) {
$info = completenessInfo($pdo, $t, (int)$r['id']);
if (!$info) {
continue;
}
if ($level !== '' && $info['level'] !== $level) {
continue;
}
$missingFields = array_merge($info['missing_core'], $info['missing_important'], $info['missing_supp']);
$all[] = [
'table' => $t,
'table_label' => $cfg['label'],
'id' => (int)$r['id'],
'name' => $r['name'] ?: ('#' . $r['id']),
'score' => $info['score'],
'level' => $info['level'],
'missing_fields' => array_map(function ($f) { return COMPLETENESS_LABELS[$f] ?? $f; }, $missingFields),
'created_at' => $r['created_at'] ?? null,
];
}
}
// 按完整度升序(最残缺的排前面),再按时间倒序
usort($all, function ($a, $b) {
if ($a['score'] !== $b['score']) {
return $a['score'] <=> $b['score'];
}
return strcmp($b['created_at'] ?? '', $a['created_at'] ?? '');
});
if ($keyword !== '') {
$all = array_values(array_filter($all, function ($r) use ($keyword) {
return mb_strpos($r['name'], $keyword) !== false;
}));
}
$total = count($all);
$offset = ($page - 1) * $limit;
$list = array_slice($all, $offset, $limit);
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
+144
View File
@@ -0,0 +1,144 @@
<?php
/**
* 软碎片补全提交接口 POST /api/fragment/soft_update.php
* 入参:table(persons|companies|social_accounts|media)/ id / 需补全字段(各表白名单)
* 逻辑:更新主表字段 → 重新计算完整度 → 达标则 is_incomplete=0
* 特殊:persons 的 phone/email 写 social_accounts(platform=phone/email)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/logger.php';
require_once __DIR__ . '/../common/helpers.php';
require_once __DIR__ . '/../common/completeness.php';
checkAjax();
checkPermission('preliminary');
$table = trim($_POST['table'] ?? '');
$id = (int)($_POST['id'] ?? 0);
if (!in_array($table, ['persons', 'companies', 'social_accounts', 'media'], true) || $id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
/** 按白名单收集 POST 字段 */
$fields = [];
$collect = function ($whitelist) use (&$fields) {
foreach ($whitelist as $k => $type) {
if (!isset($_POST[$k])) {
continue;
}
$v = trim((string)$_POST[$k]);
if ($v === '') {
$fields[$k] = null;
continue;
}
switch ($type) {
case 'i':
$fields[$k] = (int)$v;
break;
case 'd':
$fields[$k] = (strtotime($v) !== false) ? date('Y-m-d', strtotime($v)) : null;
break;
default:
$fields[$k] = $v;
}
}
};
$isComplete = false;
switch ($table) {
case 'persons':
$collect([
'full_name' => 's', 'gender' => 's', 'nationality' => 's', 'education' => 's',
'graduated_from' => 's', 'hometown' => 's', 'work_location' => 's',
'id_type' => 's', 'id_number' => 's', 'source_channel' => 's', 'source_detail' => 's',
]);
if (!empty($fields)) {
[$sets, $params] = buildUpdate($fields);
$params[] = $id;
$pdo->prepare("UPDATE persons SET $sets WHERE id = ?")->execute($params);
}
// phone/email 写 social_accounts(有则更新,无则插入)
foreach (['phone', 'email'] as $plat) {
if (!isset($_POST[$plat])) {
continue;
}
$v = trim((string)$_POST[$plat]);
$exist = $pdo->prepare("SELECT id FROM social_accounts WHERE owner_type = 'person' AND owner_id = ? AND platform = ?");
$exist->execute([$id, $plat]);
$accId = (int)$exist->fetchColumn();
if ($accId > 0) {
if ($v === '') {
$pdo->prepare("DELETE FROM social_accounts WHERE id = ?")->execute([$accId]);
} else {
$pdo->prepare("UPDATE social_accounts SET account_id = ? WHERE id = ?")->execute([$v, $accId]);
}
} elseif ($v !== '') {
$pdo->prepare(
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, is_primary, is_defult)
VALUES ('person', ?, ?, ?, 1, 1)"
)->execute([$id, $plat, $v]);
}
}
$info = updateIncomplete($pdo, 'persons', $id);
updateAccountsIncomplete($pdo, 'person', $id);
break;
case 'companies':
$collect([
'display_name' => 's', 'name_zh' => 's', 'name_en' => 's', 'business_role' => 's',
'country' => 's', 'registration_number' => 's', 'address' => 's', 'legal_form' => 's',
'legal_representative' => 's', 'business_scope' => 's', 'established_date' => 'd',
'registered_capital' => 's', 'industry' => 's', 'industry_subdivision' => 's',
'latest_employee_count' => 'i', 'latest_annual_revenue' => 's', 'is_listed' => 'i',
'stock_code' => 's', 'website' => 's',
]);
if (!empty($fields)) {
[$sets, $params] = buildUpdate($fields);
$params[] = $id;
$pdo->prepare("UPDATE companies SET $sets WHERE id = ?")->execute($params);
}
$info = updateIncomplete($pdo, 'companies', $id);
break;
case 'social_accounts':
$collect([
'platform' => 's', 'account_id' => 's', 'is_primary' => 'i', 'profile_url' => 's', 'remark' => 's',
]);
if (!empty($fields)) {
[$sets, $params] = buildUpdate($fields);
$params[] = $id;
$pdo->prepare("UPDATE social_accounts SET $sets WHERE id = ?")->execute($params);
}
$info = updateIncomplete($pdo, 'social_accounts', $id);
break;
case 'media':
$collect([
'account_level' => 's', 'content_categories' => 's', 'follower_count' => 'i',
'avg_read_count' => 'i', 'certification_type' => 's', 'special_requirements' => 's', 'media_remark' => 's',
]);
if (!empty($fields)) {
[$sets, $params] = buildUpdate($fields);
$params[] = $id;
$pdo->prepare("UPDATE media_commercial_attributes SET $sets WHERE social_account_id = ?")->execute($params);
}
$info = updateIncomplete($pdo, 'media', $id);
updateIncomplete($pdo, 'social_accounts', $id);
break;
}
if (!$info) {
Response::error('记录不存在');
}
logCurrent('complete', 'fragment', $table, $id, ['fields' => array_keys($fields), 'score' => $info['score']]);
Response::success([
'score' => $info['score'],
'level' => $info['level'],
'is_complete' => $info['is_complete'],
], $info['is_complete'] ? '补全完成' : '已更新,完整度 ' . $info['score'] . '%');
+49
View File
@@ -0,0 +1,49 @@
<?php
/**
* 碎片处理中心 - 顶部指标卡接口 GET /api/fragment/stats_overview.php
* 返回:{ hard_total 硬碎片总数, soft_total 软碎片总数, month_new 本月新增,
* month_processed 本月处理, pending 待处理碎片 }
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('preliminary');
$pdo = DB::getInstance()->getPdo();
// 硬碎片:preliminary_data 中未终结(未转换/未废弃)的记录
$hardTotal = (int)$pdo->query(
"SELECT COUNT(*) FROM preliminary_data WHERE is_active = 1 AND status NOT IN ('已转换','已废弃')"
)->fetchColumn();
// 软碎片:四张主表 is_incomplete=1 之和
$softTotal = 0;
foreach (['companies', 'persons', 'social_accounts', 'media_commercial_attributes'] as $t) {
$softTotal += (int)$pdo->query("SELECT COUNT(*) FROM `$t` WHERE is_incomplete = 1")->fetchColumn();
}
// 本月新增(硬碎片按 created_at)
$monthNew = (int)$pdo->query(
"SELECT COUNT(*) FROM preliminary_data WHERE is_active = 1 AND created_at >= DATE_FORMAT(CURDATE(), '%Y-%m-01')"
)->fetchColumn();
// 本月处理(已转换/已废弃且 processed_at 在本月)
$monthProcessed = (int)$pdo->query(
"SELECT COUNT(*) FROM preliminary_data
WHERE is_active = 1 AND status IN ('已转换','已废弃')
AND processed_at >= DATE_FORMAT(CURDATE(), '%Y-%m-01')"
)->fetchColumn();
// 待处理碎片
$pending = (int)$pdo->query(
"SELECT COUNT(*) FROM preliminary_data WHERE is_active = 1 AND status = '待处理'"
)->fetchColumn();
Response::success([
'hard_total' => $hardTotal,
'soft_total' => $softTotal,
'month_new' => $monthNew,
'month_processed' => $monthProcessed,
'pending' => $pending,
]);