v1.0.19: 修复人员编辑入口缺失(person.js addPerson/editPerson) + 渠道明细数量字段名(analysis.php cnt) + CDP全量验证43项PASS

This commit is contained in:
nanguaboss
2026-08-08 19:29:14 +08:00
parent 2cb688ed46
commit 1c8b374a72
40 changed files with 2313 additions and 658 deletions
+85
View File
@@ -0,0 +1,85 @@
<?php
/**
* 渠道效能分析接口 GET /api/channel/analysis.php?year=2025
* v1.0.18 渠道管理 - 渠道效能分析:
* - 企业/人员两个维度,分别按 source_channel(渠道字段)分组计数、按数量降序、计算百分比
* - 每个渠道附带 source_detail TOP10 排名(来源/数量)
* - year 为空 = 全部年份;否则按创建年份过滤
* 返回:{ years, company: [{channel, count, percent, details:[{source,count}]}], person: [...] }
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('channel');
$year = trim($_REQUEST['year'] ?? '');
if ($year !== '' && (!ctype_digit($year) || (int)$year < 2000 || (int)$year > 2100)) {
$year = '';
}
$pdo = DB::getInstance()->getPdo();
// 可选年份(企业/人员创建年份并集)
$years = $pdo->query(
"SELECT DISTINCT y FROM (
SELECT YEAR(created_at) AS y FROM companies WHERE is_active = 1 AND created_at IS NOT NULL
UNION
SELECT YEAR(created_at) AS y FROM persons WHERE is_active = 1 AND created_at IS NOT NULL
) t ORDER BY y DESC"
)->fetchAll(PDO::FETCH_COLUMN);
/** 单维度渠道分析 */
function channelAnalysis($pdo, $table, $year)
{
$where = 'is_active = 1 AND source_channel IS NOT NULL AND source_channel <> \'\'';
$params = [];
if ($year !== '') {
$where .= ' AND YEAR(created_at) = ?';
$params[] = (int)$year;
}
$stmt = $pdo->prepare(
"SELECT source_channel AS channel, COUNT(*) AS cnt
FROM $table WHERE $where
GROUP BY source_channel ORDER BY cnt DESC"
);
$stmt->execute($params);
$rows = $stmt->fetchAll();
$total = 0;
foreach ($rows as $r) {
$total += (int)$r['cnt'];
}
$result = [];
foreach ($rows as $r) {
$channel = $r['channel'];
// 该渠道 source_detail TOP10
$detailStmt = $pdo->prepare(
"SELECT COALESCE(NULLIF(TRIM(source_detail), ''), '(未填写)') AS source, COUNT(*) AS cnt
FROM $table
WHERE is_active = 1 AND source_channel = ?" . ($year !== '' ? ' AND YEAR(created_at) = ?' : '') . "
GROUP BY source ORDER BY cnt DESC LIMIT 10"
);
$dParams = [$channel];
if ($year !== '') {
$dParams[] = (int)$year;
}
$detailStmt->execute($dParams);
$result[] = [
'channel' => $channel,
'count' => (int)$r['cnt'],
'percent' => $total > 0 ? round(((int)$r['cnt'] / $total) * 100, 1) : 0,
'details' => $detailStmt->fetchAll(),
];
}
return $result;
}
Response::success([
'years' => $years,
'company' => channelAnalysis($pdo, 'companies', $year),
'person' => channelAnalysis($pdo, 'persons', $year),
]);
-36
View File
@@ -1,36 +0,0 @@
<?php
/**
* 渠道删除接口 POST /api/channel/delete.php
* 入参:id(或 ids 逗号分隔批量)
*/
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('channel');
$id = (int)($_POST['id'] ?? 0);
$ids = trim($_POST['ids'] ?? '');
$idList = [];
if ($id > 0) $idList[] = $id;
if ($ids !== '') {
foreach (explode(',', $ids) as $v) {
$v = (int)trim($v);
if ($v > 0) $idList[] = $v;
}
}
$idList = array_unique($idList);
if (!$idList) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$in = implode(',', array_fill(0, count($idList), '?'));
$stmt = $pdo->prepare("DELETE FROM channels WHERE id IN ($in)");
$stmt->execute($idList);
$affected = $stmt->rowCount();
logCurrent('delete', 'channel', 'channels', null, ['ids' => $idList]);
Response::success(['affected' => $affected], "已删除 $affected 个渠道");
-50
View File
@@ -1,50 +0,0 @@
<?php
/**
* 渠道列表接口 GET/POST /api/channel/list.php
* 参数:page / limit / keyword / channel_type / is_active
* 数据源:channels 表(见 sql/system_tables.sql 第5节)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('channel');
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
$type = trim($_REQUEST['channel_type'] ?? '');
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
$where = ['is_active = 1'];
$params = [];
if ($active !== null) {
$where = ['is_active = ?'];
$params = [$active];
}
if ($keyword !== '') {
$where[] = '(channel_name LIKE ? OR contact_person LIKE ? OR contact_phone LIKE ? OR contact_email LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like, $like, $like);
}
if ($type !== '') { $where[] = 'channel_type = ?'; $params[] = $type; }
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM channels WHERE $whereSql");
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, channel_name, channel_type, contact_person, contact_phone, contact_email,
efficiency_score, remark, is_active, created_at, updated_at
FROM channels
WHERE $whereSql
ORDER BY id DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
+27
View File
@@ -0,0 +1,27 @@
<?php
/**
* 渠道新增计划删除接口 POST /api/channel/plan_delete.php
* 入参:id(必填);软删除(is_active = 0)
*/
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('channel');
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("UPDATE channel_plans SET is_active = 0 WHERE id = ?");
$stmt->execute([$id]);
if ($stmt->rowCount() === 0) {
Response::error('计划不存在');
}
logCurrent('delete', 'channel_plan', 'channel_plans', $id, ['soft_delete' => true]);
Response::success(null, '删除成功');
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* 渠道新增计划列表接口 GET /api/channel/plan_list.php
* v1.0.18 渠道管理 - 渠道新增计划
* 参数:page / limit / channel_type / status / keyword
* 返回:{ list, total, page, limit }(list 含 remaining_days 剩余天数)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('channel');
[$page, $limit] = pageParams();
$channelType = trim($_REQUEST['channel_type'] ?? '');
$status = trim($_REQUEST['status'] ?? '');
$keyword = trim($_REQUEST['keyword'] ?? '');
$where = ['is_active = 1'];
$params = [];
if ($channelType !== '') {
$where[] = 'channel_type = ?';
$params[] = $channelType;
}
if ($status !== '') {
$where[] = 'status = ?';
$params[] = $status;
}
if ($keyword !== '') {
$where[] = '(source_detail LIKE ? OR industry LIKE ? OR remark LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like, $like);
}
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM channel_plans WHERE $whereSql");
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, channel_type, source_detail, industry, start_date, end_date, remark, status, created_at, updated_at
FROM channel_plans
WHERE $whereSql
ORDER BY id DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
// 剩余天数:距时间窗口结束日(end_date)的天数,已过期为 0
$today = strtotime(date('Y-m-d'));
foreach ($list as &$row) {
$row['remaining_days'] = 0;
if (!empty($row['end_date']) && strtotime($row['end_date']) !== false) {
$diff = (int)ceil((strtotime($row['end_date']) - $today) / 86400);
$row['remaining_days'] = max(0, $diff);
}
}
unset($row);
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
+55
View File
@@ -0,0 +1,55 @@
<?php
/**
* 渠道新增计划保存接口 POST /api/channel/plan_save.php
* 入参:id(可选,编辑时传)/ channel_type(渠道类别=source_channel 枚举,必填)/
* source_detail / industry / start_date / end_date / remark / status(待启动|已执行|错过)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/helpers.php';
require_once __DIR__ . '/../common/logger.php';
checkAjax();
checkPermission('channel');
$channelType = trim($_POST['channel_type'] ?? '');
if ($channelType === '') {
Response::error('渠道类别(channel_type)为必填项', 400);
}
$status = trim($_POST['status'] ?? '待启动');
if (!in_array($status, ['待启动', '已执行', '错过'], true)) {
$status = '待启动';
}
$id = (int)($_POST['id'] ?? 0);
$pdo = DB::getInstance()->getPdo();
$fields = [
'channel_type' => $channelType,
'source_detail' => trim($_POST['source_detail'] ?? '') !== '' ? trim($_POST['source_detail']) : null,
'industry' => trim($_POST['industry'] ?? '') !== '' ? trim($_POST['industry']) : null,
'start_date' => trim($_POST['start_date'] ?? '') !== '' ? $_POST['start_date'] : null,
'end_date' => trim($_POST['end_date'] ?? '') !== '' ? $_POST['end_date'] : null,
'remark' => trim($_POST['remark'] ?? '') !== '' ? trim($_POST['remark']) : null,
'status' => $status,
];
if ($id > 0) {
$check = $pdo->prepare("SELECT id FROM channel_plans WHERE id = ? AND is_active = 1");
$check->execute([$id]);
if (!$check->fetch()) {
Response::error('计划不存在');
}
[$sets, $params] = buildUpdate($fields);
$params[] = $id;
$pdo->prepare("UPDATE channel_plans SET $sets WHERE id = ?")->execute($params);
logCurrent('update', 'channel_plan', 'channel_plans', $id, $fields);
Response::success(null, '更新成功');
}
[$sql, $params] = buildInsert($fields);
$pdo->prepare("INSERT INTO channel_plans $sql")->execute($params);
$newId = (int)$pdo->lastInsertId();
logCurrent('add', 'channel_plan', 'channel_plans', $newId, $fields);
Response::success(['id' => $newId], '新增成功');
-53
View File
@@ -1,53 +0,0 @@
<?php
/**
* 渠道新增/编辑接口 POST /api/channel/save.php
* 入参:id(编辑时必传)/ channel_name / channel_type / contact_person / contact_phone / contact_email / efficiency_score / remark
* 数据源:channels 表(见 sql/system_tables.sql 第5节)
*/
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';
checkAjax();
checkPermission('channel');
$id = (int)($_POST['id'] ?? 0);
$channelName = trim($_POST['channel_name'] ?? '');
if ($channelName === '') {
Response::error('渠道名称为必填项', 400);
}
$fields = [
'channel_name' => $channelName,
'channel_type' => trim($_POST['channel_type'] ?? '') ?: null,
'contact_person' => trim($_POST['contact_person'] ?? '') ?: null,
'contact_phone' => trim($_POST['contact_phone'] ?? '') ?: null,
'contact_email' => trim($_POST['contact_email'] ?? '') ?: null,
'remark' => trim($_POST['remark'] ?? '') ?: null,
];
$score = (float)($_POST['efficiency_score'] ?? 0);
$fields['efficiency_score'] = max(0, min(100, $score));
$pdo = DB::getInstance()->getPdo();
if ($id > 0) {
$check = $pdo->prepare("SELECT * FROM channels WHERE id = ?");
$check->execute([$id]);
$old = $check->fetch();
if (!$old) {
Response::error('渠道不存在');
}
[$sets, $params] = buildUpdate($fields);
$params[] = $id;
$pdo->prepare("UPDATE channels SET $sets WHERE id = ?")->execute($params);
logCurrent('update', 'channel', 'channels', $id, ['before' => $old, 'after' => $fields]);
Response::success(['id' => $id], '更新成功');
}
[$sql, $params] = buildInsert($fields);
$pdo->prepare("INSERT INTO channels $sql")->execute($params);
$newId = (int)$pdo->lastInsertId();
logCurrent('add', 'channel', 'channels', $newId, $fields);
Response::success(['id' => $newId], '新增成功');
-44
View File
@@ -1,44 +0,0 @@
<?php
/**
* 渠道年度统计接口 GET /api/channel/stats.php?year=2025
* 返回:指定年度(默认今年)各渠道效率统计 + 年度汇总
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('channel');
$year = (int)($_REQUEST['year'] ?? date('Y'));
if ($year < 2000 || $year > 2100) {
$year = (int)date('Y');
}
$pdo = DB::getInstance()->getPdo();
$rows = $pdo->prepare(
"SELECT id, channel_name, channel_type, efficiency_score, created_at
FROM channels
WHERE is_active = 1 AND YEAR(created_at) = ?
ORDER BY efficiency_score DESC"
);
$rows->execute([$year]);
$list = $rows->fetchAll();
$summary = [
'channel_count' => count($list),
'avg_score' => 0,
'max_score' => 0,
'min_score' => 0,
];
if ($list) {
$scores = array_column($list, 'efficiency_score');
$summary['avg_score'] = round(array_sum($scores) / count($scores), 2);
$summary['max_score'] = (float)max($scores);
$summary['min_score'] = (float)min($scores);
}
// 可用年度(用于前端下拉)
$years = $pdo->query("SELECT DISTINCT YEAR(created_at) AS y FROM channels WHERE is_active = 1 ORDER BY y DESC")->fetchAll(PDO::FETCH_COLUMN);
Response::success(['year' => $year, 'list' => $list, 'summary' => $summary, 'years' => $years]);