v1.0.33: 全局唯一性校验 - 手机号/邮箱/身份证/企业注册号/社媒账号ID/主页链接/企业名称 新增与修改时校验,重复弹窗提示并跳转定位(后端duplicate_check引擎+code 1001,前端弹窗+person/company/media页?id=跳转)

This commit is contained in:
nanguaboss
2026-08-09 22:27:05 +08:00
parent de1c227727
commit 76593d4dce
15 changed files with 393 additions and 17 deletions
+240
View File
@@ -0,0 +1,240 @@
<?php
/**
* 全局唯一性校验(2026-08-09)
* 手机号码 / 邮箱 / 身份证号码 / 企业注册号 / 社媒账号ID / 主页链接 / 企业名称
* 新增、修改时必须调用;重复时返回重复记录信息(供前端弹窗 + 跳转定位)
* 说明:
* - 手机/邮箱存在 social_accounts(platform=phone/email)
* - 社媒账号ID 同平台内唯一(wechat/douyin 等,不含 phone/email)
* - 主页链接 social_accounts.profile_url 全局唯一(非空才校验)
* - 企业名称 name_zh 或 display_name 任一相同即重复
*/
require_once __DIR__ . '/db.php';
/** 字段中文名 */
const DUP_LABELS = [
'phone' => '手机号码',
'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;
}
+13
View File
@@ -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');