v1.0.40: 碎片处理板块调整 - 区块改名(硬数据新增与处理/软数据碎片处理)+软碎片列表时间列改updated_at(social_accounts补列迁移)+硬数据新增加全字段唯一性校验(企业名/手机/邮箱/身份证/账号ID同平台/主页链接)+格式校验(身份证/主页URL)+extra_info存储
This commit is contained in:
@@ -11,6 +11,7 @@ require_once __DIR__ . '/../common/response.php';
|
|||||||
require_once __DIR__ . '/../common/auth.php';
|
require_once __DIR__ . '/../common/auth.php';
|
||||||
require_once __DIR__ . '/../common/logger.php';
|
require_once __DIR__ . '/../common/logger.php';
|
||||||
require_once __DIR__ . '/../common/validate.php';
|
require_once __DIR__ . '/../common/validate.php';
|
||||||
|
require_once __DIR__ . '/../common/duplicate_check.php';
|
||||||
|
|
||||||
checkAjax();
|
checkAjax();
|
||||||
checkPermission('preliminary');
|
checkPermission('preliminary');
|
||||||
@@ -24,6 +25,10 @@ $companyName = trim($_POST['company_name'] ?? '');
|
|||||||
$personName = trim($_POST['person_name'] ?? '');
|
$personName = trim($_POST['person_name'] ?? '');
|
||||||
$phone = trim($_POST['contact_phone'] ?? '');
|
$phone = trim($_POST['contact_phone'] ?? '');
|
||||||
$email = trim($_POST['contact_email'] ?? '');
|
$email = trim($_POST['contact_email'] ?? '');
|
||||||
|
$idNumber = trim($_POST['id_number'] ?? '');
|
||||||
|
$platform = trim($_POST['platform'] ?? '');
|
||||||
|
$accountId = trim($_POST['account_id'] ?? '');
|
||||||
|
$profileUrl = trim($_POST['profile_url'] ?? '');
|
||||||
$category = trim($_POST['target_product_category'] ?? '');
|
$category = trim($_POST['target_product_category'] ?? '');
|
||||||
$description = trim($_POST['description'] ?? '');
|
$description = trim($_POST['description'] ?? '');
|
||||||
$sourceChannel = trim($_POST['source_channel'] ?? '');
|
$sourceChannel = trim($_POST['source_channel'] ?? '');
|
||||||
@@ -31,21 +36,53 @@ if ($sourceChannel === '') {
|
|||||||
$sourceChannel = '手动录入';
|
$sourceChannel = '手动录入';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式校验:手机 / 邮箱
|
// 格式校验:手机 / 邮箱 / 身份证 / 主页链接
|
||||||
checkFieldFormat('phone', $phone);
|
checkFieldFormat('phone', $phone);
|
||||||
checkFieldFormat('email', $email);
|
checkFieldFormat('email', $email);
|
||||||
|
checkFieldFormat('id_number', $idNumber);
|
||||||
|
if ($profileUrl !== '' && !preg_match('/^https?:\/\//i', $profileUrl)) {
|
||||||
|
Response::error('主页链接格式不正确(应以 http:// 或 https:// 开头)', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 唯一性校验(收集全部重复):企业名称 / 手机 / 邮箱 / 身份证 / 账号ID(同平台)/ 主页链接
|
||||||
|
$pdo = DB::getInstance()->getPdo();
|
||||||
|
$dups = [];
|
||||||
|
if ($companyName !== '') {
|
||||||
|
$dup = findDuplicate($pdo, 'company_name', $companyName);
|
||||||
|
if ($dup) $dups[] = $dup;
|
||||||
|
}
|
||||||
|
$dups = array_merge($dups, checkSingleAccountDuplicate($pdo, $phone !== '' ? 'phone' : '', $phone, $profileUrl));
|
||||||
|
if ($email !== '') {
|
||||||
|
$dup = findDuplicate($pdo, 'email', $email);
|
||||||
|
if ($dup) $dups[] = $dup;
|
||||||
|
}
|
||||||
|
if ($idNumber !== '') {
|
||||||
|
$dup = findDuplicate($pdo, 'id_number', $idNumber);
|
||||||
|
if ($dup) $dups[] = $dup;
|
||||||
|
}
|
||||||
|
if ($accountId !== '' && $platform !== '') {
|
||||||
|
$dup = findDuplicate($pdo, 'account_id', $accountId, [], $platform);
|
||||||
|
if ($dup) $dups[] = $dup;
|
||||||
|
}
|
||||||
|
if (!empty($dups)) Response::duplicates($dups);
|
||||||
|
|
||||||
|
// 额外信息(身份证/社媒平台/账号ID/主页链接)存 extra_info
|
||||||
|
$extra = [];
|
||||||
|
if ($idNumber !== '') $extra['id_number'] = $idNumber;
|
||||||
|
if ($platform !== '') $extra['platform'] = $platform;
|
||||||
|
if ($accountId !== '') $extra['account_id'] = $accountId;
|
||||||
|
if ($profileUrl !== '') $extra['profile_url'] = $profileUrl;
|
||||||
|
|
||||||
// 至少有一项内容才能录为碎片
|
// 至少有一项内容才能录为碎片
|
||||||
if ($companyName === '' && $personName === '' && $phone === '' && $email === '' && $category === '' && $description === '') {
|
if ($companyName === '' && $personName === '' && $phone === '' && $email === '' && $category === '' && $description === '' && !$extra) {
|
||||||
Response::error('请至少填写一项内容', 400);
|
Response::error('请至少填写一项内容', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdo = DB::getInstance()->getPdo();
|
|
||||||
$pdo->prepare(
|
$pdo->prepare(
|
||||||
"INSERT INTO preliminary_data
|
"INSERT INTO preliminary_data
|
||||||
(source_type, company_name, person_name, contact_phone, contact_email,
|
(source_type, company_name, person_name, contact_phone, contact_email,
|
||||||
target_product_category, description, source_channel, recorded_by, status)
|
target_product_category, description, source_channel, recorded_by, status, extra_info)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, '待处理')"
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, '待处理', ?)"
|
||||||
)->execute([
|
)->execute([
|
||||||
$sourceType,
|
$sourceType,
|
||||||
$companyName !== '' ? $companyName : null,
|
$companyName !== '' ? $companyName : null,
|
||||||
@@ -56,6 +93,7 @@ $pdo->prepare(
|
|||||||
$description !== '' ? $description : null,
|
$description !== '' ? $description : null,
|
||||||
$sourceChannel,
|
$sourceChannel,
|
||||||
$_SESSION['username'] ?? null,
|
$_SESSION['username'] ?? null,
|
||||||
|
$extra ? json_encode($extra, JSON_UNESCAPED_UNICODE) : null,
|
||||||
]);
|
]);
|
||||||
$newId = (int)$pdo->lastInsertId();
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
@@ -65,6 +103,7 @@ logCurrent('add', 'fragment', 'preliminary_data', $newId, [
|
|||||||
'person_name' => $personName,
|
'person_name' => $personName,
|
||||||
'phone' => $phone,
|
'phone' => $phone,
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
|
'extra' => $extra,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Response::success(['id' => $newId], '新增成功');
|
Response::success(['id' => $newId], '新增成功');
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ foreach ($tableMap as $t => $cfg) {
|
|||||||
}
|
}
|
||||||
if ($t === 'media') {
|
if ($t === 'media') {
|
||||||
$rows = $pdo->query(
|
$rows = $pdo->query(
|
||||||
"SELECT m.social_account_id AS id, sa.account_id AS name, sa.created_at AS created_at
|
"SELECT m.social_account_id AS id, sa.account_id AS name, m.updated_at AS updated_at
|
||||||
FROM media_commercial_attributes m
|
FROM media_commercial_attributes m
|
||||||
LEFT JOIN social_accounts sa ON sa.id = m.social_account_id
|
LEFT JOIN social_accounts sa ON sa.id = m.social_account_id
|
||||||
WHERE m.is_incomplete = 1"
|
WHERE m.is_incomplete = 1"
|
||||||
)->fetchAll();
|
)->fetchAll();
|
||||||
} else {
|
} else {
|
||||||
$rows = $pdo->query("SELECT id, {$cfg['name']} AS name, created_at FROM `$t` WHERE is_incomplete = 1")->fetchAll();
|
$rows = $pdo->query("SELECT id, {$cfg['name']} AS name, updated_at FROM `$t` WHERE is_incomplete = 1")->fetchAll();
|
||||||
}
|
}
|
||||||
foreach ($rows as $r) {
|
foreach ($rows as $r) {
|
||||||
$info = completenessInfo($pdo, $t, (int)$r['id']);
|
$info = completenessInfo($pdo, $t, (int)$r['id']);
|
||||||
@@ -65,17 +65,17 @@ foreach ($tableMap as $t => $cfg) {
|
|||||||
'optional' => $info['layers']['optional']['rate'],
|
'optional' => $info['layers']['optional']['rate'],
|
||||||
],
|
],
|
||||||
'missing_fields' => array_map(function ($f) { return COMPLETENESS_LABELS[$f] ?? $f; }, $missingFields),
|
'missing_fields' => array_map(function ($f) { return COMPLETENESS_LABELS[$f] ?? $f; }, $missingFields),
|
||||||
'created_at' => $r['created_at'] ?? null,
|
'updated_at' => $r['updated_at'] ?? null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 排序:整体完整度升序(最残缺在前),同分按时间倒序
|
// 排序:整体完整度升序(最残缺在前),同分按更新时间倒序
|
||||||
usort($all, function ($a, $b) {
|
usort($all, function ($a, $b) {
|
||||||
if ($a['overall'] !== $b['overall']) {
|
if ($a['overall'] !== $b['overall']) {
|
||||||
return $a['overall'] <=> $b['overall'];
|
return $a['overall'] <=> $b['overall'];
|
||||||
}
|
}
|
||||||
return strcmp($b['created_at'] ?? '', $a['created_at'] ?? '');
|
return strcmp($b['updated_at'] ?? '', $a['updated_at'] ?? '');
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($keyword !== '') {
|
if ($keyword !== '') {
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- SuperLink Web - v1.0.40 数据库结构变更
|
||||||
|
-- social_accounts 增加 updated_at(软碎片列表「上一次更新」时间用)
|
||||||
|
-- ============================================================
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `social_accounts`
|
||||||
|
ADD COLUMN `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最近更新时间' AFTER `created_at`;
|
||||||
+1
-1
@@ -5,7 +5,7 @@ var BASE_URL = '/api/';
|
|||||||
var PAGE_SIZE = 20;
|
var PAGE_SIZE = 20;
|
||||||
|
|
||||||
/** 系统版本号(logo旁展示):修改代码后运行 tools/bump_version.php 自动递增 */
|
/** 系统版本号(logo旁展示):修改代码后运行 tools/bump_version.php 自动递增 */
|
||||||
var APP_VERSION = 'v1.0.39';
|
var APP_VERSION = 'v1.0.40';
|
||||||
|
|
||||||
/** 页脚版权/备案信息(在 config.js 中修改) */
|
/** 页脚版权/备案信息(在 config.js 中修改) */
|
||||||
var FOOTER_TEXT = '© 2026 SuperLink 管理系统 版权所有 | 备案号:请替换为真实备案号';
|
var FOOTER_TEXT = '© 2026 SuperLink 管理系统 版权所有 | 备案号:请替换为真实备案号';
|
||||||
|
|||||||
+14
-5
@@ -42,7 +42,7 @@ $(function () {
|
|||||||
// ================= 硬碎片 =================
|
// ================= 硬碎片 =================
|
||||||
'<div class="card" style="margin-top:14px;">' +
|
'<div class="card" style="margin-top:14px;">' +
|
||||||
' <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">' +
|
' <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">' +
|
||||||
' <b style="font-size:15px;color:#1e2a3a;">【硬碎片】</b>' +
|
' <b style="font-size:15px;color:#1e2a3a;">【硬数据新增与处理】</b>' +
|
||||||
' <span style="font-size:12px;color:#8a94a6;">关键字段缺失、未入主表的原始线索(不含已转换/已废弃)</span>' +
|
' <span style="font-size:12px;color:#8a94a6;">关键字段缺失、未入主表的原始线索(不含已转换/已废弃)</span>' +
|
||||||
' </div>' +
|
' </div>' +
|
||||||
' <div class="toolbar">' +
|
' <div class="toolbar">' +
|
||||||
@@ -68,7 +68,7 @@ $(function () {
|
|||||||
// ================= 软碎片 =================
|
// ================= 软碎片 =================
|
||||||
'<div class="card" style="margin-top:14px;">' +
|
'<div class="card" style="margin-top:14px;">' +
|
||||||
' <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">' +
|
' <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">' +
|
||||||
' <b style="font-size:15px;color:#1e2a3a;">【软碎片】</b>' +
|
' <b style="font-size:15px;color:#1e2a3a;">【软数据碎片处理】</b>' +
|
||||||
' <span style="font-size:12px;color:#8a94a6;">已入主表但完整度低于阈值的记录(is_incomplete=1)</span>' +
|
' <span style="font-size:12px;color:#8a94a6;">已入主表但完整度低于阈值的记录(is_incomplete=1)</span>' +
|
||||||
' </div>' +
|
' </div>' +
|
||||||
' <div class="toolbar">' +
|
' <div class="toolbar">' +
|
||||||
@@ -83,7 +83,7 @@ $(function () {
|
|||||||
' <button class="btn" id="soft-reset">重置</button>' +
|
' <button class="btn" id="soft-reset">重置</button>' +
|
||||||
' </div>' +
|
' </div>' +
|
||||||
' <div class="table-wrap"><table class="grid">' +
|
' <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>' +
|
' <thead><tr><th>完整度</th><th>缺失层级</th><th>名称</th><th>各层完整度</th><th>缺失字段</th><th>所属表</th><th>上一次更新</th><th>操作</th></tr></thead>' +
|
||||||
' <tbody id="soft-tbody"></tbody>' +
|
' <tbody id="soft-tbody"></tbody>' +
|
||||||
' </table></div>' +
|
' </table></div>' +
|
||||||
' <div class="pagination" id="soft-pagination"></div>' +
|
' <div class="pagination" id="soft-pagination"></div>' +
|
||||||
@@ -187,7 +187,7 @@ $(function () {
|
|||||||
'<td style="font-size:12px;color:#5a6472;white-space:nowrap;">' + layerHtml + '</td>' +
|
'<td style="font-size:12px;color:#5a6472;white-space:nowrap;">' + layerHtml + '</td>' +
|
||||||
'<td style="max-width:240px;">' + escHtml(missing) + '</td>' +
|
'<td style="max-width:240px;">' + escHtml(missing) + '</td>' +
|
||||||
'<td>' + escHtml(r.table_label) + '</td>' +
|
'<td>' + escHtml(r.table_label) + '</td>' +
|
||||||
'<td>' + fmtDate(r.created_at) + '</td>' +
|
'<td>' + fmtDate(r.updated_at) + '</td>' +
|
||||||
'<td class="ops"><a onclick="completeSoft(\'' + r.table + '\',' + r.id + ')">补全</a></td></tr>';
|
'<td class="ops"><a onclick="completeSoft(\'' + r.table + '\',' + r.id + ')">补全</a></td></tr>';
|
||||||
});
|
});
|
||||||
if (!softRows.length) {
|
if (!softRows.length) {
|
||||||
@@ -211,8 +211,15 @@ $(function () {
|
|||||||
' <div class="form-item" id="ha-f-person_name"><label>联系人</label><input type="text" name="person_name" placeholder="如:张三"></div>' +
|
' <div class="form-item" id="ha-f-person_name"><label>联系人</label><input type="text" name="person_name" placeholder="如:张三"></div>' +
|
||||||
' <div class="form-item"><label>联系电话</label><input type="text" name="contact_phone" placeholder="如:13812345678"></div>' +
|
' <div class="form-item"><label>联系电话</label><input type="text" name="contact_phone" placeholder="如:13812345678"></div>' +
|
||||||
' <div class="form-item"><label>联系邮箱</label><input type="text" name="contact_email" placeholder="如:a@b.com"></div>' +
|
' <div class="form-item"><label>联系邮箱</label><input type="text" name="contact_email" placeholder="如:a@b.com"></div>' +
|
||||||
|
' <div class="form-item" id="ha-f-idnumber"><label>身份证号码</label><input type="text" name="id_number" placeholder="15 或 18 位"></div>' +
|
||||||
' <div class="form-item" id="ha-f-category"><label>意向产品品类</label><input type="text" name="target_product_category" placeholder="如:工业机器人"></div>' +
|
' <div class="form-item" id="ha-f-category"><label>意向产品品类</label><input type="text" name="target_product_category" placeholder="如:工业机器人"></div>' +
|
||||||
' <div class="form-item"><label>来源渠道</label><select name="source_channel" id="ha-source"><option value="手动录入">手动录入</option></select></div>' +
|
' <div class="form-item"><label>来源渠道</label><select name="source_channel" id="ha-source"><option value="手动录入">手动录入</option></select></div>' +
|
||||||
|
' <div class="form-item" id="ha-f-platform"><label>社媒平台</label><select name="platform" id="ha-platform"><option value="">-</option>' +
|
||||||
|
' <option value="wechat">微信</option><option value="weibo">微博</option><option value="douyin">抖音</option>' +
|
||||||
|
' <option value="bilibili">B站</option><option value="linkedin">领英</option><option value="xiaohongshu">小红书</option>' +
|
||||||
|
' <option value="other">其他</option></select></div>' +
|
||||||
|
' <div class="form-item" id="ha-f-account"><label>账号ID</label><input type="text" name="account_id" placeholder="社媒账号ID"></div>' +
|
||||||
|
' <div class="form-item" id="ha-f-profile"><label>主页链接</label><input type="text" name="profile_url" placeholder="https://..."></div>' +
|
||||||
' <div class="form-item full"><label>原始描述</label><textarea name="description" style="height:70px;" placeholder="记录原始线索信息"></textarea></div>' +
|
' <div class="form-item full"><label>原始描述</label><textarea name="description" style="height:70px;" placeholder="记录原始线索信息"></textarea></div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="dialog-footer"><button type="button" class="btn" id="ha-cancel">取消</button>' +
|
'<div class="dialog-footer"><button type="button" class="btn" id="ha-cancel">取消</button>' +
|
||||||
@@ -253,9 +260,11 @@ $(function () {
|
|||||||
if (name) fd[name] = $(this).val();
|
if (name) fd[name] = $(this).val();
|
||||||
});
|
});
|
||||||
fd.source_type = $('#ha-type').val();
|
fd.source_type = $('#ha-type').val();
|
||||||
// 格式校验:手机 / 邮箱
|
// 格式校验:手机 / 邮箱 / 身份证
|
||||||
if (fd.contact_phone && !validatePhoneCN(fd.contact_phone)) { Dialog.error('手机号码格式不正确(应为国内 11 位手机号,如 13812345678)'); return; }
|
if (fd.contact_phone && !validatePhoneCN(fd.contact_phone)) { Dialog.error('手机号码格式不正确(应为国内 11 位手机号,如 13812345678)'); return; }
|
||||||
if (fd.contact_email && !validateEmail(fd.contact_email)) { Dialog.error('邮箱格式不正确'); return; }
|
if (fd.contact_email && !validateEmail(fd.contact_email)) { Dialog.error('邮箱格式不正确'); return; }
|
||||||
|
if (fd.id_number && !validateIdCard(fd.id_number)) { Dialog.error('身份证号码格式不正确(应为 15 或 18 位)'); return; }
|
||||||
|
if (fd.profile_url && !/^https?:\/\//.test(fd.profile_url)) { Dialog.error('主页链接格式不正确(应以 http:// 或 https:// 开头)'); return; }
|
||||||
httpPost('fragment/hard_add.php', fd).then(function () {
|
httpPost('fragment/hard_add.php', fd).then(function () {
|
||||||
Dialog.success('新增成功', function () {
|
Dialog.success('新增成功', function () {
|
||||||
Dialog.close(idx);
|
Dialog.close(idx);
|
||||||
|
|||||||
Reference in New Issue
Block a user