v1.0.19: 修复人员编辑入口缺失(person.js addPerson/editPerson) + 渠道明细数量字段名(analysis.php cnt) + CDP全量验证43项PASS
This commit is contained in:
@@ -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),
|
||||
]);
|
||||
@@ -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 个渠道");
|
||||
@@ -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]);
|
||||
@@ -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, '删除成功');
|
||||
@@ -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]);
|
||||
@@ -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], '新增成功');
|
||||
@@ -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], '新增成功');
|
||||
@@ -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]);
|
||||
Reference in New Issue
Block a user