v1.0.27: 碎片处理中心(完整方案) - 完整度计算引擎(四表分层/阈值)+is_incomplete标记(写操作挂钩)+api/fragment八接口(统计/硬碎片列表详情转换废弃/软碎片列表详情补全)+fragment.html前端(指标卡+硬软碎片双区块)
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
/**
|
||||
* 完整度计算引擎(碎片处理方案 v1.0.27)
|
||||
* 公式:完整度 = 100 - (缺失核心字段数×20) - (缺失重要字段数×8) - (缺失补充字段数×2),下限 0%
|
||||
* 阈值:persons/companies ≥65%,social_accounts ≥60%,media_commercial_attributes ≥55%
|
||||
* 说明:persons 的 phone/email 为虚拟字段(来自 social_accounts 关联记录)
|
||||
*/
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
/** 四表字段分层定义(key => 层级),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);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ 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('company');
|
||||
@@ -61,5 +62,9 @@ if (array_key_exists('financials', $_POST) || array_key_exists('certifications',
|
||||
}
|
||||
}
|
||||
|
||||
// 完整度检查:企业主表 + 其 social_accounts
|
||||
updateIncomplete($pdo, 'companies', $newId);
|
||||
updateAccountsIncomplete($pdo, 'company', $newId);
|
||||
|
||||
logCurrent('add', 'company', 'companies', $newId, $data);
|
||||
Response::success(['id' => $newId], '新增成功');
|
||||
|
||||
@@ -10,6 +10,7 @@ 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('company');
|
||||
@@ -75,6 +76,8 @@ while (($row = fgetcsv($handle)) !== false) {
|
||||
($rec['is_listed'] ?? 0) ? 1 : 0, $rec['stock_code'] ?? null,
|
||||
$rec['source_channel'] ?? null, $rec['source_detail'] ?? null,
|
||||
]);
|
||||
// 完整度检查(导入每行入库后)
|
||||
updateIncomplete($pdo, 'companies', (int)$pdo->lastInsertId());
|
||||
$inserted++;
|
||||
} catch (Exception $e) {
|
||||
$failed++;
|
||||
|
||||
@@ -10,6 +10,7 @@ require_once __DIR__ . '/../common/response.php';
|
||||
require_once __DIR__ . '/../common/auth.php';
|
||||
require_once __DIR__ . '/../common/helpers.php';
|
||||
require_once __DIR__ . '/../common/logger.php';
|
||||
require_once __DIR__ . '/../common/completeness.php';
|
||||
|
||||
checkAjax();
|
||||
checkPermission('company');
|
||||
@@ -44,5 +45,9 @@ $ins->execute([
|
||||
]);
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
// 完整度检查:新官媒账号 + 所属企业全部联系方式
|
||||
updateIncomplete($pdo, 'social_accounts', $newId);
|
||||
updateAccountsIncomplete($pdo, 'company', $companyId);
|
||||
|
||||
logCurrent('add', 'official_media', 'social_accounts', $newId, ['company_id' => $companyId, 'platform' => $platform, 'account_id' => $accountId]);
|
||||
Response::success(['id' => $newId], '新增成功');
|
||||
|
||||
@@ -8,6 +8,7 @@ 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('company');
|
||||
@@ -76,6 +77,10 @@ if ($hasFinancials || $hasCertifications || $hasAccounts) {
|
||||
}
|
||||
}
|
||||
|
||||
// 完整度检查:企业主表 + 其 social_accounts
|
||||
updateIncomplete($pdo, 'companies', $id);
|
||||
updateAccountsIncomplete($pdo, 'company', $id);
|
||||
|
||||
if (!empty($data)) {
|
||||
logCurrent('update', 'company', 'companies', $id, ['before' => $old, 'after' => $data]);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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], '转换成功');
|
||||
@@ -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);
|
||||
@@ -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, '已废弃');
|
||||
@@ -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]);
|
||||
@@ -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'],
|
||||
]);
|
||||
@@ -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]);
|
||||
@@ -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'] . '%');
|
||||
@@ -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,
|
||||
]);
|
||||
@@ -9,6 +9,7 @@ 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('media');
|
||||
@@ -53,5 +54,8 @@ if (!empty($attr)) {
|
||||
$pdo->prepare("INSERT INTO media_commercial_attributes $attrSql")->execute($attrParams);
|
||||
}
|
||||
|
||||
// 完整度检查:媒体账号 + 商业属性
|
||||
updateMediaIncomplete($pdo, $newId);
|
||||
|
||||
logCurrent('add', 'media', 'social_accounts', $newId, ['data' => $data, 'attr' => $attr]);
|
||||
Response::success(['id' => $newId], '新增成功');
|
||||
|
||||
@@ -9,6 +9,7 @@ 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('media');
|
||||
@@ -85,6 +86,8 @@ while (($row = fgetcsv($handle)) !== false) {
|
||||
$rec['certification_type'] ?? null,
|
||||
]);
|
||||
}
|
||||
// 完整度检查(导入每行入库后)
|
||||
updateMediaIncomplete($pdo, $newId);
|
||||
$inserted++;
|
||||
} catch (Exception $e) {
|
||||
$failed++;
|
||||
|
||||
@@ -8,6 +8,7 @@ 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('media');
|
||||
@@ -50,5 +51,8 @@ if (!empty($attr)) {
|
||||
}
|
||||
}
|
||||
|
||||
// 完整度检查:媒体账号 + 商业属性
|
||||
updateMediaIncomplete($pdo, $id);
|
||||
|
||||
logCurrent('update', 'media', 'social_accounts', $id, ['before' => $old, 'after' => $data, 'attr' => $attr]);
|
||||
Response::success(null, '更新成功');
|
||||
|
||||
@@ -9,6 +9,7 @@ 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('person');
|
||||
@@ -55,5 +56,9 @@ if (is_array($experiences)) {
|
||||
savePersonExperiences($pdo, $newId, $experiences);
|
||||
}
|
||||
|
||||
// 完整度检查:人员主表 + 其 social_accounts
|
||||
updateIncomplete($pdo, 'persons', $newId);
|
||||
updateAccountsIncomplete($pdo, 'person', $newId);
|
||||
|
||||
logCurrent('add', 'person', 'persons', $newId, ['data' => $data, 'contacts' => $contacts]);
|
||||
Response::success(['id' => $newId], '新增成功');
|
||||
|
||||
@@ -9,6 +9,7 @@ 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('person');
|
||||
@@ -72,6 +73,9 @@ while (($row = fgetcsv($handle)) !== false) {
|
||||
if (!empty($rec['email'])) {
|
||||
$insAccount->execute([$newId, 'email', $rec['email']]);
|
||||
}
|
||||
// 完整度检查(导入每行入库后)
|
||||
updateIncomplete($pdo, 'persons', $newId);
|
||||
updateAccountsIncomplete($pdo, 'person', $newId);
|
||||
$inserted++;
|
||||
} catch (Exception $e) {
|
||||
$failed++;
|
||||
|
||||
@@ -8,6 +8,7 @@ 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('person');
|
||||
@@ -63,5 +64,9 @@ if (array_key_exists('experiences', $_POST)) {
|
||||
savePersonExperiences($pdo, $id, $experiences);
|
||||
}
|
||||
|
||||
// 完整度检查:人员主表 + 其 social_accounts
|
||||
updateIncomplete($pdo, 'persons', $id);
|
||||
updateAccountsIncomplete($pdo, 'person', $id);
|
||||
|
||||
logCurrent('update', 'person', 'persons', $id, ['before' => $old, 'after' => $data, 'contacts_replaced' => $contactsChanged]);
|
||||
Response::success(null, '更新成功');
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>碎片处理中心 - SuperLink</title>
|
||||
<link rel="stylesheet" href="static/libs/layer.css">
|
||||
<link rel="stylesheet" href="static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<script src="static/libs/jquery-3.6.0.min.js"></script>
|
||||
<script src="static/libs/layer.js"></script>
|
||||
<script src="static/js/config.js"></script>
|
||||
<script src="static/js/common.js"></script>
|
||||
<script src="static/js/fragment.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
-- ============================================================
|
||||
-- SuperLink Web - v1.0.27 数据库结构变更
|
||||
-- 碎片处理完整方案:软碎片管理(完整度计算与待补全标记)
|
||||
-- 四张主表增加 is_incomplete(0=完整,1=待补全)
|
||||
-- ============================================================
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `companies`
|
||||
ADD COLUMN `is_incomplete` TINYINT(1) UNSIGNED DEFAULT 0 COMMENT '是否待补全(0=完整,1=待补全)' AFTER `website`;
|
||||
|
||||
ALTER TABLE `persons`
|
||||
ADD COLUMN `is_incomplete` TINYINT(1) UNSIGNED DEFAULT 0 COMMENT '是否待补全(0=完整,1=待补全)' AFTER `work_location`;
|
||||
|
||||
ALTER TABLE `social_accounts`
|
||||
ADD COLUMN `is_incomplete` TINYINT(1) UNSIGNED DEFAULT 0 COMMENT '是否待补全(0=完整,1=待补全)' AFTER `is_active`;
|
||||
|
||||
ALTER TABLE `media_commercial_attributes`
|
||||
ADD COLUMN `is_incomplete` TINYINT(1) UNSIGNED DEFAULT 0 COMMENT '是否待补全(0=完整,1=待补全)' AFTER `media_remark`;
|
||||
@@ -737,6 +737,7 @@ table.grid .ops a:hover { text-decoration: underline; }
|
||||
.tag-green { background: #eafaf1; color: #27ae60; }
|
||||
.tag-red { background: #fdecea; color: #e74c3c; }
|
||||
.tag-orange { background: #fef5e7; color: #f39c12; }
|
||||
.tag-blue { background: #eaf2fd; color: #2a5298; }
|
||||
.tag-gray { background: #f0f2f6; color: #8a94a6; }
|
||||
|
||||
/* ---------- 空状态 ---------- */
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ var BASE_URL = '/api/';
|
||||
var PAGE_SIZE = 20;
|
||||
|
||||
/** 系统版本号(logo旁展示):修改代码后运行 tools/bump_version.php 自动递增 */
|
||||
var APP_VERSION = 'v1.0.26';
|
||||
var APP_VERSION = 'v1.0.27';
|
||||
|
||||
/** 页脚版权/备案信息(在 config.js 中修改) */
|
||||
var FOOTER_TEXT = '© 2026 SuperLink 管理系统 版权所有 | 备案号:请替换为真实备案号';
|
||||
@@ -35,7 +35,7 @@ var ICONS = {
|
||||
/** 菜单标识 -> 权限显示信息(角色权限标签等用) */
|
||||
var MENU_MAP = {
|
||||
'dashboard': { name: '综合概览', icon: 'dashboard', url: 'dashboard.html' },
|
||||
'preliminary': { name: '碎片处理', icon: 'preliminary', url: 'preliminary.html' },
|
||||
'preliminary': { name: '碎片处理', icon: 'preliminary', url: 'fragment.html' },
|
||||
'company': { name: '企业数据', icon: 'company', url: 'company.html' },
|
||||
'person': { name: '人员数据', icon: 'person', url: 'person.html' },
|
||||
'need': { name: '需求转盘', icon: 'need', url: 'need.html' },
|
||||
@@ -58,7 +58,7 @@ var MENU_MAP = {
|
||||
*/
|
||||
var MENU_GROUPS = [
|
||||
{ key: 'dashboard', name: '综合概览', icon: 'dashboard', url: 'dashboard.html' },
|
||||
{ key: 'preliminary', name: '碎片处理', icon: 'preliminary', url: 'preliminary.html' },
|
||||
{ key: 'preliminary', name: '碎片处理', icon: 'preliminary', url: 'fragment.html' },
|
||||
{
|
||||
key: 'data', name: '数据管理', icon: 'data', children: [
|
||||
{ key: 'company', name: '企业数据', icon: 'company', url: 'company.html' },
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* fragment.js - 碎片处理中心(v1.0.27)
|
||||
* 布局:顶部指标卡 + 【硬碎片】独立区块 + 【软碎片】独立区块(不用 Tab)
|
||||
* 硬碎片:preliminary_data 中未终结记录(完整度/类型/内容摘要/来源/录入人/时间/状态/操作)
|
||||
* 软碎片:四张主表 is_incomplete=1 记录(完整度/缺失层级标签/名称/缺失字段/所属表/时间/操作)
|
||||
*/
|
||||
$(function () {
|
||||
renderShell('碎片处理中心', '数据管理 / 碎片处理');
|
||||
renderPage();
|
||||
initPage('preliminary', function (user) {
|
||||
if (!checkPagePermission('preliminary')) return;
|
||||
loadStats();
|
||||
loadHardList(1);
|
||||
loadSoftList(1);
|
||||
loadDicts();
|
||||
});
|
||||
|
||||
var hardPage = 1, softPage = 1;
|
||||
var hardRows = [], softRows = [];
|
||||
|
||||
/** 缺失层级标签样式 */
|
||||
function levelTagHtml(level) {
|
||||
if (level === 'core') return '<span class="tag tag-red">缺核心</span>';
|
||||
if (level === 'important') return '<span class="tag tag-orange">缺重要</span>';
|
||||
if (level === 'supplementary') return '<span class="tag tag-blue">待补充</span>';
|
||||
return '<span class="tag tag-green">完整</span>';
|
||||
}
|
||||
|
||||
/** 完整度进度条 + 百分比 */
|
||||
function completenessHtml(score) {
|
||||
var color = score >= 80 ? '#2f8f46' : (score >= 65 ? '#e6a23c' : '#d9534f');
|
||||
return '<div style="display:flex;align-items:center;gap:6px;min-width:90px;">' +
|
||||
'<div style="flex:1;height:6px;background:#eef0f4;border-radius:3px;overflow:hidden;">' +
|
||||
'<div style="height:100%;width:' + score + '%;background:' + color + ';border-radius:3px;"></div></div>' +
|
||||
'<span style="font-size:12px;color:' + color + ';white-space:nowrap;">' + score + '%</span></div>';
|
||||
}
|
||||
|
||||
/** 渲染页面内容:指标卡 + 硬碎片 + 软碎片 两个独立区块 */
|
||||
function renderPage() {
|
||||
$('#page-content').html(
|
||||
'<div class="stat-cards" id="stat-cards"></div>' +
|
||||
// ================= 硬碎片 =================
|
||||
'<div class="card" style="margin-top:14px;">' +
|
||||
' <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">' +
|
||||
' <b style="font-size:15px;color:#1e2a3a;">【硬碎片】</b>' +
|
||||
' <span style="font-size:12px;color:#8a94a6;">关键字段缺失、未入主表的原始线索(不含已转换/已废弃)</span>' +
|
||||
' </div>' +
|
||||
' <div class="toolbar">' +
|
||||
' <select id="hard-type"><option value="">全部类型</option>' +
|
||||
' <option value="company">企业</option><option value="person">人员</option>' +
|
||||
' <option value="product">产品</option><option value="need">需求</option><option value="mixed">混合</option></select>' +
|
||||
' <select id="hard-source"><option value="">全部来源</option></select>' +
|
||||
' <select id="hard-status"><option value="">全部状态</option>' +
|
||||
' <option value="待处理">待处理</option><option value="待分配">待分配</option>' +
|
||||
' <option value="处理中">处理中</option><option value="已拒收">已拒收</option></select>' +
|
||||
' <input type="text" id="hard-keyword" placeholder="公司/姓名/电话/邮箱/描述关键词">' +
|
||||
' <button class="btn btn-primary" id="hard-search">搜索</button>' +
|
||||
' <button class="btn" id="hard-reset">重置</button>' +
|
||||
' </div>' +
|
||||
' <div class="table-wrap"><table class="grid">' +
|
||||
' <thead><tr><th>完整度</th><th>类型</th><th>内容摘要</th><th>来源</th><th>录入人</th><th>录入时间</th><th>状态</th><th>操作</th></tr></thead>' +
|
||||
' <tbody id="hard-tbody"></tbody>' +
|
||||
' </table></div>' +
|
||||
' <div class="pagination" id="hard-pagination"></div>' +
|
||||
'</div>' +
|
||||
// ================= 软碎片 =================
|
||||
'<div class="card" style="margin-top:14px;">' +
|
||||
' <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">' +
|
||||
' <b style="font-size:15px;color:#1e2a3a;">【软碎片】</b>' +
|
||||
' <span style="font-size:12px;color:#8a94a6;">已入主表但完整度低于阈值的记录(is_incomplete=1)</span>' +
|
||||
' </div>' +
|
||||
' <div class="toolbar">' +
|
||||
' <select id="soft-table"><option value="">全部所属表</option>' +
|
||||
' <option value="persons">人员</option><option value="companies">企业</option>' +
|
||||
' <option value="social_accounts">联系方式</option><option value="media">媒体</option></select>' +
|
||||
' <select id="soft-level"><option value="">全部缺失层级</option>' +
|
||||
' <option value="core">缺核心</option><option value="important">缺重要</option>' +
|
||||
' <option value="supplementary">待补充</option></select>' +
|
||||
' <input type="text" id="soft-keyword" placeholder="名称关键词">' +
|
||||
' <button class="btn btn-primary" id="soft-search">搜索</button>' +
|
||||
' <button class="btn" id="soft-reset">重置</button>' +
|
||||
' </div>' +
|
||||
' <div class="table-wrap"><table class="grid">' +
|
||||
' <thead><tr><th>完整度</th><th>缺失层级</th><th>名称</th><th>缺失字段</th><th>所属表</th><th>录入时间</th><th>操作</th></tr></thead>' +
|
||||
' <tbody id="soft-tbody"></tbody>' +
|
||||
' </table></div>' +
|
||||
' <div class="pagination" id="soft-pagination"></div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
$('#hard-search').on('click', function () { loadHardList(1); });
|
||||
$('#hard-keyword').on('keydown', function (e) { if (e.keyCode === 13) loadHardList(1); });
|
||||
$('#hard-reset').on('click', function () {
|
||||
$('#hard-type').val(''); $('#hard-source').val(''); $('#hard-status').val(''); $('#hard-keyword').val('');
|
||||
loadHardList(1);
|
||||
});
|
||||
$('#soft-search').on('click', function () { loadSoftList(1); });
|
||||
$('#soft-keyword').on('keydown', function (e) { if (e.keyCode === 13) loadSoftList(1); });
|
||||
$('#soft-reset').on('click', function () {
|
||||
$('#soft-table').val(''); $('#soft-level').val(''); $('#soft-keyword').val('');
|
||||
loadSoftList(1);
|
||||
});
|
||||
}
|
||||
|
||||
/** 来源渠道下拉选项 */
|
||||
function loadDicts() {
|
||||
httpGet('common/dicts.php', { type: 'all' }).then(function (d) {
|
||||
var html = '<option value="">全部来源</option>';
|
||||
(d.source_channels || []).forEach(function (c) {
|
||||
html += '<option value="' + escHtml(c.name) + '">' + escHtml(c.name) + '</option>';
|
||||
});
|
||||
$('#hard-source').html(html);
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
/* ================= 顶部指标卡 ================= */
|
||||
function loadStats() {
|
||||
httpGet('fragment/stats_overview.php').then(function (d) {
|
||||
$('#stat-cards').html(
|
||||
'<div class="stat-card c-blue"><div class="num">' + d.hard_total + '</div><div class="label">硬碎片总数</div></div>' +
|
||||
'<div class="stat-card c-teal"><div class="num">' + d.soft_total + '</div><div class="label">软碎片总数</div></div>' +
|
||||
'<div class="stat-card c-green"><div class="num">' + d.month_new + '</div><div class="label">本月新增</div></div>' +
|
||||
'<div class="stat-card c-orange"><div class="num">' + d.month_processed + '</div><div class="label">本月处理</div></div>' +
|
||||
'<div class="stat-card c-red"><div class="num">' + d.pending + '</div><div class="label">待处理碎片</div></div>'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/* ================= 硬碎片列表 ================= */
|
||||
function loadHardList(page) {
|
||||
hardPage = page;
|
||||
var params = {
|
||||
page: page,
|
||||
source_type: $('#hard-type').val(),
|
||||
source_channel: $('#hard-source').val(),
|
||||
status: $('#hard-status').val(),
|
||||
keyword: $.trim($('#hard-keyword').val())
|
||||
};
|
||||
httpGet('fragment/hard_list.php', params).then(function (d) {
|
||||
hardRows = d.list || [];
|
||||
var html = '';
|
||||
hardRows.forEach(function (r) {
|
||||
html += '<tr>' +
|
||||
'<td>' + completenessHtml(r.completeness) + '</td>' +
|
||||
'<td>' + escHtml(r.source_type_label) + '</td>' +
|
||||
'<td title="' + escHtml(r.summary) + '" style="max-width:280px;">' + escHtml(r.summary || '-') + '</td>' +
|
||||
'<td>' + escHtml(r.source_channel || '-') + '</td>' +
|
||||
'<td>' + escHtml(r.recorded_by || '-') + '</td>' +
|
||||
'<td>' + fmtDate(r.created_at) + '</td>' +
|
||||
'<td>' + escHtml(r.status || '-') + '</td>' +
|
||||
'<td class="ops">' +
|
||||
' <a onclick="viewHard(' + r.id + ')">查看</a>' +
|
||||
' <a onclick="convertHard(' + r.id + ')">补全录入</a>' +
|
||||
' <a onclick="discardHard(' + r.id + ')">废弃</a>' +
|
||||
'</td></tr>';
|
||||
});
|
||||
if (!hardRows.length) {
|
||||
html = '<tr><td colspan="8" class="empty-tip" style="padding:30px 0;">暂无硬碎片</td></tr>';
|
||||
}
|
||||
$('#hard-tbody').html(html);
|
||||
renderPagination($('#hard-pagination'), d, loadHardList);
|
||||
});
|
||||
}
|
||||
|
||||
/* ================= 软碎片列表 ================= */
|
||||
function loadSoftList(page) {
|
||||
softPage = page;
|
||||
var params = {
|
||||
page: page,
|
||||
table: $('#soft-table').val(),
|
||||
level: $('#soft-level').val(),
|
||||
keyword: $.trim($('#soft-keyword').val())
|
||||
};
|
||||
httpGet('fragment/soft_list.php', params).then(function (d) {
|
||||
softRows = d.list || [];
|
||||
var html = '';
|
||||
softRows.forEach(function (r) {
|
||||
var missing = (r.missing_fields || []).join('、') || '-';
|
||||
html += '<tr>' +
|
||||
'<td>' + completenessHtml(r.score) + '</td>' +
|
||||
'<td>' + levelTagHtml(r.level) + '</td>' +
|
||||
'<td>' + escHtml(r.name) + '</td>' +
|
||||
'<td style="max-width:260px;">' + escHtml(missing) + '</td>' +
|
||||
'<td>' + escHtml(r.table_label) + '</td>' +
|
||||
'<td>' + fmtDate(r.created_at) + '</td>' +
|
||||
'<td class="ops"><a onclick="completeSoft(\'' + r.table + '\',' + r.id + ')">补全</a></td></tr>';
|
||||
});
|
||||
if (!softRows.length) {
|
||||
html = '<tr><td colspan="7" class="empty-tip" style="padding:30px 0;">暂无软碎片</td></tr>';
|
||||
}
|
||||
$('#soft-tbody').html(html);
|
||||
renderPagination($('#soft-pagination'), d, loadSoftList);
|
||||
});
|
||||
}
|
||||
|
||||
/* ================= 硬碎片:查看 ================= */
|
||||
window.viewHard = function (id) {
|
||||
httpGet('fragment/hard_detail.php', { id: id }).then(function (d) {
|
||||
var missing = [].concat(d.missing.core || [], d.missing.important || [], d.missing.supplementary || []);
|
||||
var rowHtml = '';
|
||||
var fields = [
|
||||
['类型', d.source_type_label], ['公司名称', d.company_name], ['联系人', d.person_name],
|
||||
['联系电话', d.contact_phone], ['联系邮箱', d.contact_email], ['意向品类', d.target_product_category],
|
||||
['来源渠道', d.source_channel], ['录入人', d.recorded_by], ['状态', d.status],
|
||||
['录入时间', d.created_at], ['处理时间', d.processed_at]
|
||||
];
|
||||
fields.forEach(function (f) {
|
||||
rowHtml += '<div style="display:flex;margin:6px 0;"><span style="width:80px;color:#8a94a6;">' + f[0] + '</span>' +
|
||||
'<span>' + escHtml(f[1] || '-') + '</span></div>';
|
||||
});
|
||||
if (d.description) {
|
||||
rowHtml += '<div style="margin:6px 0;"><span style="color:#8a94a6;">原始描述</span><div style="margin-top:4px;padding:8px;background:#f7f8fa;border-radius:4px;font-size:12px;max-height:120px;overflow:auto;">' + escHtml(d.description) + '</div></div>';
|
||||
}
|
||||
Dialog.open({
|
||||
title: '硬碎片详情(完整度 ' + d.completeness + '%)',
|
||||
area: ['560px', 'auto'],
|
||||
content: '<div style="padding:16px 22px;">' + rowHtml + '</div>',
|
||||
btn: ['关闭']
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/* ================= 硬碎片:补全录入(转换) ================= */
|
||||
window.convertHard = function (id) {
|
||||
httpGet('fragment/hard_detail.php', { id: id }).then(function (d) {
|
||||
var typeSel = '<select id="cv-type">' +
|
||||
'<option value="company"' + (d.source_type === 'company' || d.source_type === 'mixed' ? ' selected' : '') + '>企业</option>' +
|
||||
'<option value="person"' + (d.source_type === 'person' ? ' selected' : '') + '>人员</option>' +
|
||||
'<option value="product"' + (d.source_type === 'product' ? ' selected' : '') + '>产品</option>' +
|
||||
'<option value="need"' + (d.source_type === 'need' ? ' selected' : '') + '>需求</option></select>';
|
||||
var content =
|
||||
'<form id="cv-form" style="padding:16px 22px 4px;">' +
|
||||
'<div class="form-grid">' +
|
||||
' <div class="form-item"><label>转换目标<span class="req">*</span></label>' + typeSel + '</div>' +
|
||||
' <div class="form-item" id="cv-f-company_name"><label>公司名称</label><input type="text" name="company_name" value="' + escHtml(d.company_name || '') + '"></div>' +
|
||||
' <div class="form-item" id="cv-f-person_name"><label>联系人</label><input type="text" name="person_name" value="' + escHtml(d.person_name || '') + '"></div>' +
|
||||
' <div class="form-item"><label>联系电话</label><input type="text" name="contact_phone" value="' + escHtml(d.contact_phone || '') + '"></div>' +
|
||||
' <div class="form-item"><label>联系邮箱</label><input type="text" name="contact_email" value="' + escHtml(d.contact_email || '') + '"></div>' +
|
||||
' <div class="form-item" id="cv-f-category"><label>产品品类</label><input type="text" name="target_product_category" value="' + escHtml(d.target_product_category || '') + '"></div>' +
|
||||
' <div class="form-item" id="cv-f-industry"><label>行业</label><input type="text" name="industry" placeholder="转换为企业时填写"></div>' +
|
||||
' <div class="form-item" id="cv-f-needcat"><label>需求类别</label><input type="text" name="need_category" placeholder="如:设备采购"></div>' +
|
||||
' <div class="form-item" id="cv-f-scenario"><label>应用场景</label><input type="text" name="application_scenario" placeholder="如:码垛"></div>' +
|
||||
' <div class="form-item full"><label>描述/补充说明</label><textarea name="description" style="height:60px;">' + escHtml(d.description || '') + '</textarea></div>' +
|
||||
'</div>' +
|
||||
'<div class="dialog-footer"><button type="button" class="btn" id="cv-cancel">取消</button>' +
|
||||
'<button type="submit" class="btn btn-primary">确认转换</button></div>' +
|
||||
'</form>';
|
||||
|
||||
var idx = Dialog.open({
|
||||
title: '硬碎片补全录入',
|
||||
area: ['640px', 'auto'],
|
||||
content: content,
|
||||
btn: false
|
||||
});
|
||||
|
||||
function syncFields() {
|
||||
var t = $('#cv-type').val();
|
||||
$('#cv-f-company_name').toggle(t !== 'person');
|
||||
$('#cv-f-person_name').toggle(t === 'person');
|
||||
$('#cv-f-category').toggle(t === 'product' || t === 'need');
|
||||
$('#cv-f-industry').toggle(t === 'company');
|
||||
$('#cv-f-needcat').toggle(t === 'need');
|
||||
$('#cv-f-scenario').toggle(t === 'need');
|
||||
}
|
||||
$('#cv-type').on('change', syncFields);
|
||||
syncFields();
|
||||
$('#cv-cancel').on('click', function () { Dialog.close(idx); });
|
||||
|
||||
$('#cv-form').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var fd = {};
|
||||
$(this).find('input, select, textarea').each(function () {
|
||||
var name = $(this).attr('name');
|
||||
if (name) fd[name] = $(this).val();
|
||||
});
|
||||
fd.id = id;
|
||||
fd.target_type = $('#cv-type').val();
|
||||
httpPost('fragment/hard_convert.php', fd).then(function () {
|
||||
Dialog.success('转换成功', function () {
|
||||
Dialog.close(idx);
|
||||
loadStats();
|
||||
loadHardList(hardPage);
|
||||
loadSoftList(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/* ================= 硬碎片:废弃 ================= */
|
||||
window.discardHard = function (id) {
|
||||
Dialog.confirm('确定废弃该碎片吗?废弃后不可恢复。', function () {
|
||||
httpPost('fragment/hard_discard.php', { id: id }).then(function () {
|
||||
Dialog.success('已废弃', function () {
|
||||
loadStats();
|
||||
loadHardList(hardPage);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/* ================= 软碎片:补全 ================= */
|
||||
window.completeSoft = function (table, id) {
|
||||
httpGet('fragment/soft_detail.php', { table: table, id: id }).then(function (d) {
|
||||
var allMissing = [].concat(d.missing_core || [], d.missing_important || [], d.missing_supp || []);
|
||||
var inputs = '';
|
||||
allMissing.forEach(function (m) {
|
||||
var isDate = (m.key === 'established_date');
|
||||
inputs += '<div class="form-item"><label>' + escHtml(m.label) + (m.key === 'full_name' || m.key === 'display_name' ? '<span class="req">*</span>' : '') + '</label>' +
|
||||
'<input type="' + (isDate ? 'date' : 'text') + '" name="' + m.key + '"></div>';
|
||||
});
|
||||
var content =
|
||||
'<form id="soft-form" style="padding:16px 22px 4px;">' +
|
||||
'<div style="font-size:12px;color:#8a94a6;margin-bottom:10px;">当前完整度 <b style="color:#d9534f;">' + d.score + '%</b>(阈值 ' + d.threshold + '%),补全后自动重新计算。</div>' +
|
||||
'<div class="form-grid">' + inputs + '</div>' +
|
||||
'<div class="dialog-footer"><button type="button" class="btn" id="soft-cancel">取消</button>' +
|
||||
'<button type="submit" class="btn btn-primary">保存补全</button></div>' +
|
||||
'</form>';
|
||||
|
||||
var idx = Dialog.open({
|
||||
title: '软碎片补全 - ' + escHtml(d.name) + '(' + d.table_label + ')',
|
||||
area: ['640px', 'auto'],
|
||||
content: content,
|
||||
btn: false
|
||||
});
|
||||
$('#soft-cancel').on('click', function () { Dialog.close(idx); });
|
||||
|
||||
$('#soft-form').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var fd = { table: table, id: id };
|
||||
$(this).find('input').each(function () {
|
||||
var name = $(this).attr('name');
|
||||
if (name) fd[name] = $(this).val();
|
||||
});
|
||||
httpPost('fragment/soft_update.php', fd).then(function (res) {
|
||||
Dialog.success(res && res.is_complete ? '补全完成' : '已保存(完整度 ' + (res && res.score) + '%)', function () {
|
||||
Dialog.close(idx);
|
||||
loadStats();
|
||||
loadSoftList(softPage);
|
||||
loadHardList(hardPage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user