diff --git a/api/common/duplicate_check.php b/api/common/duplicate_check.php new file mode 100644 index 0000000..89e4122 --- /dev/null +++ b/api/common/duplicate_check.php @@ -0,0 +1,240 @@ + '手机号码', + 'email' => '邮箱', + 'id_number' => '身份证号码', + 'registration_number' => '企业注册号', + 'account_id' => '社媒账号ID', + 'profile_url' => '主页链接', + 'company_name' => '企业名称', +]; + +/** + * 查询 social_accounts 重复记录(phone/email/account_id/profile_url 共用) + * @return array|null + */ +function findSocialDuplicate(PDO $pdo, $where, $params) +{ + $sql = + "SELECT sa.id, sa.owner_type, sa.owner_id, + COALESCE(p.full_name, c.display_name) AS owner_name + FROM social_accounts sa + LEFT JOIN persons p ON p.id = sa.owner_id AND sa.owner_type = 'person' + LEFT JOIN companies c ON c.id = sa.owner_id AND sa.owner_type = 'company' + WHERE $where AND sa.is_active = 1 + LIMIT 1"; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $row = $stmt->fetch(); + if (!$row) { + return null; + } + return $row; +} + +/** + * 校验唯一性 + * @param PDO $pdo + * @param string $type phone|email|id_number|registration_number|account_id|profile_url|company_name + * @param string $value 待校验值 + * @param array $excludeIds 排除自身记录ID(编辑时;social_accounts 传其 id / persons 传 persons.id / companies 传 companies.id) + * @param string|null $platform account_id 类型时必传(同平台内唯一) + * @return array|null {label,value,table,id,owner_type,owner_id,name,owner_label,jump} + */ +function findDuplicate(PDO $pdo, $type, $value, $excludeIds = [], $platform = null) +{ + $value = trim((string)$value); + if ($value === '') { + return null; + } + $excludeIds = array_values(array_filter(array_map('intval', (array)$excludeIds))); + + $excludeSql = ''; + $excludeParams = []; + if ($excludeIds) { + $excludeSql = ' AND id NOT IN (' . implode(',', $excludeIds) . ')'; + } + + switch ($type) { + case 'phone': + case 'email': + $row = findSocialDuplicate($pdo, "sa.platform = ? AND LOWER(sa.account_id) = LOWER(?)" . str_replace('id NOT IN', 'sa.id NOT IN', $excludeSql), array_merge([$type, $value], $excludeParams)); + if (!$row) { + return null; + } + return [ + 'label' => $type === 'phone' ? '手机号码' : '邮箱', + 'value' => $value, + 'table' => 'social_accounts', + 'id' => (int)$row['id'], + 'owner_type' => $row['owner_type'], + 'owner_id' => (int)$row['owner_id'], + 'name' => $row['owner_name'] ?: ('#' . $row['owner_id']), + 'owner_label'=> $row['owner_type'] === 'person' ? '人员' : '企业', + 'jump' => 'media.html?id=' . (int)$row['id'], + ]; + + case 'account_id': + // 社媒账号:同平台内唯一(phone/email 走专门校验) + if ($platform === null || in_array($platform, ['phone', 'email'], true)) { + return null; + } + $row = findSocialDuplicate($pdo, "sa.platform = ? AND LOWER(sa.account_id) = LOWER(?)" . str_replace('id NOT IN', 'sa.id NOT IN', $excludeSql), array_merge([$platform, $value], $excludeParams)); + if (!$row) { + return null; + } + return [ + 'label' => '社媒账号ID', + 'value' => $value, + 'table' => 'social_accounts', + 'id' => (int)$row['id'], + 'owner_type' => $row['owner_type'], + 'owner_id' => (int)$row['owner_id'], + 'name' => $row['owner_name'] ?: ('#' . $row['owner_id']), + 'owner_label'=> $row['owner_type'] === 'person' ? '人员' : '企业', + 'jump' => 'media.html?id=' . (int)$row['id'], + ]; + + case 'profile_url': + $row = findSocialDuplicate($pdo, "sa.profile_url = ? AND sa.profile_url <> ''" . str_replace('id NOT IN', 'sa.id NOT IN', $excludeSql), array_merge([$value], $excludeParams)); + if (!$row) { + return null; + } + return [ + 'label' => '主页链接', + 'value' => $value, + 'table' => 'social_accounts', + 'id' => (int)$row['id'], + 'owner_type' => $row['owner_type'], + 'owner_id' => (int)$row['owner_id'], + 'name' => $row['owner_name'] ?: ('#' . $row['owner_id']), + 'owner_label'=> $row['owner_type'] === 'person' ? '人员' : '企业', + 'jump' => 'media.html?id=' . (int)$row['id'], + ]; + + case 'id_number': + $stmt = $pdo->prepare("SELECT id, full_name FROM persons WHERE id_number = ? AND is_active = 1$excludeSql LIMIT 1"); + $stmt->execute(array_merge([$value], $excludeParams)); + $row = $stmt->fetch(); + if (!$row) { + return null; + } + return [ + 'label' => '身份证号码', + 'value' => $value, + 'table' => 'persons', + 'id' => (int)$row['id'], + 'name' => $row['full_name'] ?: ('#' . $row['id']), + 'jump' => 'person.html?id=' . (int)$row['id'], + ]; + + case 'registration_number': + $stmt = $pdo->prepare("SELECT id, display_name FROM companies WHERE registration_number = ? AND is_active = 1$excludeSql LIMIT 1"); + $stmt->execute(array_merge([$value], $excludeParams)); + $row = $stmt->fetch(); + if (!$row) { + return null; + } + return [ + 'label' => '企业注册号', + 'value' => $value, + 'table' => 'companies', + 'id' => (int)$row['id'], + 'name' => $row['display_name'] ?: ('#' . $row['id']), + 'jump' => 'company.html?id=' . (int)$row['id'], + ]; + + case 'company_name': + $stmt = $pdo->prepare("SELECT id, display_name FROM companies WHERE (name_zh = ? OR display_name = ?) AND is_active = 1$excludeSql LIMIT 1"); + $stmt->execute(array_merge([$value, $value], $excludeParams)); + $row = $stmt->fetch(); + if (!$row) { + return null; + } + return [ + 'label' => '企业名称', + 'value' => $value, + 'table' => 'companies', + 'id' => (int)$row['id'], + 'name' => $row['display_name'] ?: ('#' . $row['id']), + 'jump' => 'company.html?id=' . (int)$row['id'], + ]; + } + return null; +} + +/** + * 校验单条账号(媒体模块扁平字段) + * @param PDO $pdo + * @param string $platform + * @param string $accountId + * @param string $profileUrl + * @param array $excludeIds + * @return array|null + */ +function checkSingleAccountDuplicate(PDO $pdo, $platform, $accountId, $profileUrl = '', $excludeIds = []) +{ + $platform = trim((string)$platform); + $accountId = trim((string)$accountId); + if ($platform !== '' && $accountId !== '') { + if ($platform === 'phone' || $platform === 'email') { + $dup = findDuplicate($pdo, $platform, $accountId, $excludeIds); + } else { + $dup = findDuplicate($pdo, 'account_id', $accountId, $excludeIds, $platform); + } + if ($dup) { + return $dup; + } + } + $profileUrl = trim((string)$profileUrl); + if ($profileUrl !== '') { + $dup = findDuplicate($pdo, 'profile_url', $profileUrl, $excludeIds); + if ($dup) { + return $dup; + } + } + return null; +} + +/** + * 校验联系方式数组(persons/companies 的 contacts/accounts JSON) + * @param PDO $pdo + * @param array $accounts [{platform,account_id,profile_url,...}] + * @param array $excludeIds 整表替换时排除自身既有账号ID + * @return array|null + */ +function checkAccountsDuplicates(PDO $pdo, $accounts, $excludeIds = []) +{ + if (!is_array($accounts)) { + return null; + } + foreach ($accounts as $a) { + if (!is_array($a)) { + continue; + } + $dup = checkSingleAccountDuplicate( + $pdo, + $a['platform'] ?? '', + $a['account_id'] ?? '', + $a['profile_url'] ?? '', + $excludeIds + ); + if ($dup) { + return $dup; + } + } + return null; +} diff --git a/api/common/response.php b/api/common/response.php index 3ee48f4..4913167 100644 --- a/api/common/response.php +++ b/api/common/response.php @@ -25,6 +25,19 @@ class Response self::json(['code' => $code, 'msg' => $msg]); } + /** + * 重复数据错误(前端弹窗 + 跳转定位) + * @param array $dup 重复记录信息:{label,value,name,owner_label,jump,...} + */ + public static function duplicate($dup) + { + self::json([ + 'code' => 1001, + 'msg' => ($dup['label'] ?? '字段') . '「' . ($dup['value'] ?? '') . '」出现重复', + 'data' => $dup, + ]); + } + private static function json($payload) { header('Content-Type: application/json; charset=utf-8'); diff --git a/api/company/add.php b/api/company/add.php index 4ec2af5..8c51d8b 100644 --- a/api/company/add.php +++ b/api/company/add.php @@ -9,6 +9,7 @@ require_once __DIR__ . '/../common/auth.php'; require_once __DIR__ . '/../common/logger.php'; require_once __DIR__ . '/../common/helpers.php'; require_once __DIR__ . '/../common/completeness.php'; +require_once __DIR__ . '/../common/duplicate_check.php'; checkAjax(); checkPermission('company'); @@ -28,6 +29,23 @@ if ((int)($data['is_listed'] ?? 0) !== 1) { } $pdo = DB::getInstance()->getPdo(); + +// 唯一性校验:企业名称 / 注册号 / 联系方式(手机/邮箱/社媒账号/主页链接) +$dup = findDuplicate($pdo, 'company_name', $data['name_zh'] ?? $data['display_name'] ?? ''); +if ($dup) Response::duplicate($dup); +if (!empty($data['registration_number'])) { + $dup = findDuplicate($pdo, 'registration_number', $data['registration_number']); + if ($dup) Response::duplicate($dup); +} +if (isset($_POST['accounts'])) { + $accounts = json_decode($_POST['accounts'], true); + if (!is_array($accounts)) { + Response::error('联系方式格式错误', 400); + } + $dup = checkAccountsDuplicates($pdo, $accounts); + if ($dup) Response::duplicate($dup); +} + [$sql, $params] = buildInsert($data); $pdo->prepare("INSERT INTO companies $sql")->execute($params); $newId = (int)$pdo->lastInsertId(); @@ -53,12 +71,7 @@ if (array_key_exists('financials', $_POST) || array_key_exists('certifications', saveCompanyCertifications($pdo, $newId, $certs); } if (array_key_exists('accounts', $_POST)) { - $accounts = json_decode($_POST['accounts'], true); - if (!is_array($accounts)) { - $pdo->rollBack(); - Response::error('联系方式格式错误', 400); - } - saveCompanyAccounts($pdo, $newId, $accounts); + saveCompanyAccounts($pdo, $newId, $accounts ?? []); } $pdo->commit(); } catch (Exception $e) { diff --git a/api/company/official_media_add.php b/api/company/official_media_add.php index 9f219b3..50ae58f 100644 --- a/api/company/official_media_add.php +++ b/api/company/official_media_add.php @@ -11,6 +11,7 @@ require_once __DIR__ . '/../common/auth.php'; require_once __DIR__ . '/../common/helpers.php'; require_once __DIR__ . '/../common/logger.php'; require_once __DIR__ . '/../common/completeness.php'; +require_once __DIR__ . '/../common/duplicate_check.php'; checkAjax(); checkPermission('company'); @@ -30,6 +31,10 @@ if ((int)$chk->fetchColumn() === 0) { Response::error('企业不存在'); } +// 唯一性校验:社媒账号ID(同平台)/ 手机 / 邮箱 / 主页链接 +$dup = checkSingleAccountDuplicate($pdo, $platform, $accountId, $_POST['profile_url'] ?? ''); +if ($dup) Response::duplicate($dup); + $ins = $pdo->prepare( "INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary, is_defult, is_active) VALUES ('company', ?, ?, ?, ?, ?, 0, ?, ?)" diff --git a/api/company/update.php b/api/company/update.php index 6a00835..4ae624e 100644 --- a/api/company/update.php +++ b/api/company/update.php @@ -9,6 +9,7 @@ require_once __DIR__ . '/../common/auth.php'; require_once __DIR__ . '/../common/logger.php'; require_once __DIR__ . '/../common/helpers.php'; require_once __DIR__ . '/../common/completeness.php'; +require_once __DIR__ . '/../common/duplicate_check.php'; checkAjax(); checkPermission('company'); @@ -40,6 +41,25 @@ if (!$old) { Response::error('企业不存在'); } +// 唯一性校验(排除自身):企业名称 / 注册号 / 联系方式 +$dup = findDuplicate($pdo, 'company_name', $data['name_zh'] ?? $old['display_name'] ?? '', [$id]); +if ($dup) Response::duplicate($dup); +if (!empty($data['registration_number'])) { + $dup = findDuplicate($pdo, 'registration_number', $data['registration_number'], [$id]); + if ($dup) Response::duplicate($dup); +} +$accounts = null; +if ($hasAccounts) { + $accounts = json_decode($_POST['accounts'], true); + if (!is_array($accounts)) { + Response::error('联系方式格式错误', 400); + } + // 整表替换:排除企业自身既有联系方式ID + $ownIds = $pdo->query("SELECT id FROM social_accounts WHERE owner_type = 'company' AND owner_id = $id")->fetchAll(PDO::FETCH_COLUMN); + $dup = checkAccountsDuplicates($pdo, $accounts, $ownIds); + if ($dup) Response::duplicate($dup); +} + if (!empty($data)) { [$sets, $params] = buildUpdate($data); $params[] = $id; @@ -67,11 +87,6 @@ if ($hasFinancials || $hasCertifications || $hasAccounts) { saveCompanyCertifications($pdo, $id, $certs); } if ($hasAccounts) { - $accounts = json_decode($_POST['accounts'], true); - if (!is_array($accounts)) { - $pdo->rollBack(); - Response::error('联系方式格式错误', 400); - } saveCompanyAccounts($pdo, $id, $accounts); } $pdo->commit(); diff --git a/api/fragment/hard_convert.php b/api/fragment/hard_convert.php index 3376305..1d92bef 100644 --- a/api/fragment/hard_convert.php +++ b/api/fragment/hard_convert.php @@ -11,6 +11,7 @@ require_once __DIR__ . '/../common/response.php'; require_once __DIR__ . '/../common/auth.php'; require_once __DIR__ . '/../common/logger.php'; require_once __DIR__ . '/../common/completeness.php'; +require_once __DIR__ . '/../common/duplicate_check.php'; checkAjax(); checkPermission('preliminary'); @@ -44,6 +45,18 @@ $sourceChannel = $frag['source_channel'] ?? null; $convertedId = 0; +// 唯一性校验:按目标类型 +if ($targetType === 'company' && $companyName !== '') { + $dup = findDuplicate($pdo, 'company_name', $companyName); + if ($dup) Response::duplicate($dup); +} +if (in_array($targetType, ['person', 'company'], true)) { + $dup = checkSingleAccountDuplicate($pdo, $phone !== '' ? 'phone' : '', $phone, ''); + if ($dup) Response::duplicate($dup); + $dup = checkSingleAccountDuplicate($pdo, $email !== '' ? 'email' : '', $email, ''); + if ($dup) Response::duplicate($dup); +} + /** 创建 social_accounts(phone/email,常用) */ $saveContacts = function ($ownerType, $ownerId) use ($pdo, $phone, $email) { if ($phone !== '') { diff --git a/api/media/add.php b/api/media/add.php index 0388b27..205355f 100644 --- a/api/media/add.php +++ b/api/media/add.php @@ -10,6 +10,7 @@ require_once __DIR__ . '/../common/auth.php'; require_once __DIR__ . '/../common/logger.php'; require_once __DIR__ . '/../common/helpers.php'; require_once __DIR__ . '/../common/completeness.php'; +require_once __DIR__ . '/../common/duplicate_check.php'; checkAjax(); checkPermission('media'); @@ -42,6 +43,10 @@ if ((int)$chk->fetchColumn() > 0) { Response::error('该平台账号已存在', 400); } +// 唯一性校验:社媒账号ID(同平台)/ 手机 / 邮箱 / 主页链接 +$dup = checkSingleAccountDuplicate($pdo, $data['platform'] ?? '', $data['account_id'] ?? '', $data['profile_url'] ?? ''); +if ($dup) Response::duplicate($dup); + [$sql, $params] = buildInsert($data); $pdo->prepare("INSERT INTO social_accounts $sql")->execute($params); $newId = (int)$pdo->lastInsertId(); diff --git a/api/media/update.php b/api/media/update.php index c1d6c76..3d36d5f 100644 --- a/api/media/update.php +++ b/api/media/update.php @@ -9,6 +9,7 @@ require_once __DIR__ . '/../common/auth.php'; require_once __DIR__ . '/../common/logger.php'; require_once __DIR__ . '/../common/helpers.php'; require_once __DIR__ . '/../common/completeness.php'; +require_once __DIR__ . '/../common/duplicate_check.php'; checkAjax(); checkPermission('media'); @@ -29,6 +30,18 @@ if (!$old) { $data = extractFields(MEDIA_FIELDS); unset($data['owner_type'], $data['owner_id']); // 归属关系不允许改 +// 唯一性校验(排除自身):社媒账号ID(同平台)/ 手机 / 邮箱 / 主页链接 +if (isset($_POST['platform']) || isset($_POST['account_id']) || isset($_POST['profile_url'])) { + $dup = checkSingleAccountDuplicate( + $pdo, + $_POST['platform'] ?? ($old['platform'] ?? ''), + $_POST['account_id'] ?? ($old['account_id'] ?? ''), + $_POST['profile_url'] ?? ($old['profile_url'] ?? ''), + [$id] + ); + if ($dup) Response::duplicate($dup); +} + if (!empty($data)) { [$sets, $params] = buildUpdate($data); $params[] = $id; diff --git a/api/person/add.php b/api/person/add.php index e51bd13..971caf1 100644 --- a/api/person/add.php +++ b/api/person/add.php @@ -10,6 +10,7 @@ require_once __DIR__ . '/../common/auth.php'; require_once __DIR__ . '/../common/logger.php'; require_once __DIR__ . '/../common/helpers.php'; require_once __DIR__ . '/../common/completeness.php'; +require_once __DIR__ . '/../common/duplicate_check.php'; checkAjax(); checkPermission('person'); @@ -31,13 +32,24 @@ if ((int)$chk->fetchColumn() > 0) { Response::error('union_id 已存在,请更换'); } +// 唯一性校验:身份证号码 / 联系方式(手机/邮箱/社媒账号/主页链接) +if (!empty($data['id_number'])) { + $dup = findDuplicate($pdo, 'id_number', $data['id_number']); + if ($dup) Response::duplicate($dup); +} +$contacts = json_decode($_POST['contacts'] ?? '[]', true); +if (!is_array($contacts)) { + $contacts = []; +} +$dup = checkAccountsDuplicates($pdo, $contacts); +if ($dup) Response::duplicate($dup); + [$sql, $params] = buildInsert($data); $pdo->prepare("INSERT INTO persons $sql")->execute($params); $newId = (int)$pdo->lastInsertId(); // 写入联系方式(v1.0.16:支持 is_defult 字段,1常用/0备用) -$contacts = json_decode($_POST['contacts'] ?? '[]', true); -if (is_array($contacts)) { +if (is_array($contacts) && count($contacts) > 0) { $ins = $pdo->prepare( "INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary, is_defult) VALUES ('person', ?, ?, ?, ?, ?, ?, ?)" diff --git a/api/person/update.php b/api/person/update.php index f197fc1..cfefe90 100644 --- a/api/person/update.php +++ b/api/person/update.php @@ -9,6 +9,7 @@ require_once __DIR__ . '/../common/auth.php'; require_once __DIR__ . '/../common/logger.php'; require_once __DIR__ . '/../common/helpers.php'; require_once __DIR__ . '/../common/completeness.php'; +require_once __DIR__ . '/../common/duplicate_check.php'; checkAjax(); checkPermission('person'); @@ -29,13 +30,29 @@ if (!$old) { Response::error('人员不存在'); } -$contactsChanged = false; +// 唯一性校验(排除自身):身份证号码 / 联系方式 +if (!empty($data['id_number'])) { + $dup = findDuplicate($pdo, 'id_number', $data['id_number'], [$id]); + if ($dup) Response::duplicate($dup); +} +$contacts = null; if (isset($_POST['contacts'])) { + $contacts = json_decode($_POST['contacts'], true); + if (!is_array($contacts)) { + Response::error('联系方式格式错误', 400); + } + // 整体替换:排除人员自身既有联系方式ID + $ownIds = $pdo->query("SELECT id FROM social_accounts WHERE owner_type = 'person' AND owner_id = $id")->fetchAll(PDO::FETCH_COLUMN); + $dup = checkAccountsDuplicates($pdo, $contacts, $ownIds); + if ($dup) Response::duplicate($dup); +} + +$contactsChanged = false; +if ($contacts !== null) { $contactsChanged = true; // 整体替换联系方式:先删旧(保留非联系人性质?此处直接删除全部后重建) $pdo->prepare("DELETE FROM social_accounts WHERE owner_type = 'person' AND owner_id = ?")->execute([$id]); - $contacts = json_decode($_POST['contacts'], true); - if (is_array($contacts)) { + if (count($contacts) > 0) { $ins = $pdo->prepare( "INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary, is_defult) VALUES ('person', ?, ?, ?, ?, ?, ?, ?)" diff --git a/static/js/common.js b/static/js/common.js index 0e34958..c5f87f6 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -49,6 +49,27 @@ function handleResp(resp) { location.href = 'index.html'; return $.Deferred().reject(resp).promise(); } + // 重复数据:弹窗提示 + 跳转定位到已有记录 + if (resp && resp.code === 1001 && resp.data) { + var d = resp.data; + layer.open({ + type: 1, + title: '数据重复提示', + area: ['440px', 'auto'], + content: '