This commit is contained in:
nanguaboss
2026-08-03 00:07:01 +08:00
commit 71c6e8d9c1
112 changed files with 7815 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
<?php
/**
* 找回密码接口 POST /api/auth/forgot.php
* 入参:username / contact_email
* 说明:占位实现 —— 校验账号存在后返回成功(后续可接入 SMTP 发送重置邮件/短信验证码)。
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/logger.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
Response::error('请求方式错误', 400);
}
$username = trim($_POST['username'] ?? '');
$contactEmail = trim($_POST['contact_email'] ?? '');
if ($username === '' || $contactEmail === '') {
Response::error('请输入账号和联系邮箱');
}
if (!filter_var($contactEmail, FILTER_VALIDATE_EMAIL)) {
Response::error('邮箱格式不正确');
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT id, username, real_name FROM system_users WHERE username = ? AND is_active = 1 LIMIT 1");
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user) {
Response::error('账号不存在或已禁用');
}
// TODO: 接入 SMTP 后向 contactEmail 发送重置链接/验证码
logAction((int)$user['id'], $user['username'], 'forgot', 'auth', 'system_users', (int)$user['id'], ['contact_email' => $contactEmail]);
Response::success(null, '重置邮件已发送(占位实现,请接入SMTP后生效)');
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
* 登录接口 POST /api/auth/login.php
* 入参:username / password / slider_token(滑块验证通过后的token)
* 出参(成功):{ code:0, msg:'登录成功', data:{ real_name, role_name, permissions } }
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/logger.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
Response::error('请求方式错误', 400);
}
$username = trim($_POST['username'] ?? '');
$password = (string)($_POST['password'] ?? '');
$sliderToken = trim($_POST['slider_token'] ?? '');
// 滑块验证:前端 slider.js 拖动通过后生成的 token,服务端校验非空且格式合法
if ($sliderToken === '' || strlen($sliderToken) < 10) {
Response::error('请先完成滑块验证');
}
if ($username === '' || $password === '') {
Response::error('请输入账号和密码');
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare(
"SELECT u.*, r.role_name, r.permissions
FROM system_users u
LEFT JOIN system_roles r ON r.id = u.role_id
WHERE u.username = ? LIMIT 1"
);
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user || !hash_equals($user['password'], sha1($password))) {
Response::error('账号或密码错误');
}
if ((int)$user['is_active'] !== 1) {
Response::error('账号已被禁用,请联系管理员');
}
// 更新最后登录时间/IP
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$upd = $pdo->prepare("UPDATE system_users SET last_login_time = NOW(), last_login_ip = ? WHERE id = ?");
$upd->execute([$ip, $user['id']]);
// 写入 Session
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION['user_id'] = (int)$user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['real_name'] = $user['real_name'] ?? $user['username'];
$_SESSION['role_id'] = (int)$user['role_id'];
$_SESSION['role_name'] = $user['role_name'] ?? '';
$_SESSION['permissions'] = json_decode($user['permissions'] ?? '[]', true) ?: [];
// 操作日志
logAction((int)$user['id'], $user['username'], 'login', 'auth', 'system_users', (int)$user['id'], ['ip' => $ip]);
Response::success([
'real_name' => $_SESSION['real_name'],
'role_name' => $_SESSION['role_name'],
'permissions' => $_SESSION['permissions'],
], '登录成功');
+26
View File
@@ -0,0 +1,26 @@
<?php
/**
* 登出接口 POST /api/auth/logout.php
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/logger.php';
require_once __DIR__ . '/../common/auth.php';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!empty($_SESSION['user_id'])) {
logAction((int)$_SESSION['user_id'], $_SESSION['username'] ?? '', 'logout', 'auth', 'system_users', (int)$_SESSION['user_id']);
}
// 清空并销毁会话
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$p = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
}
session_destroy();
Response::success(null, '已退出登录');
+24
View File
@@ -0,0 +1,24 @@
<?php
/**
* 会话校验接口 GET /api/auth/session.php
* 前端页面加载时调用:未登录返回 401(前端跳转登录页),已登录返回当前用户信息。
*/
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
requireLogin();
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT last_login_time, last_login_ip FROM system_users WHERE id = ?");
$stmt->execute([(int)$_SESSION['user_id']]);
$loginInfo = $stmt->fetch() ?: [];
Response::success([
'user_id' => (int)$_SESSION['user_id'],
'username' => $_SESSION['username'],
'real_name' => $_SESSION['real_name'],
'role_name' => $_SESSION['role_name'],
'permissions' => $_SESSION['permissions'],
'last_login_time' => $loginInfo['last_login_time'] ?? null,
'last_login_ip' => $loginInfo['last_login_ip'] ?? null,
]);
+36
View File
@@ -0,0 +1,36 @@
<?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
@@ -0,0 +1,50 @@
<?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]);
+53
View File
@@ -0,0 +1,53 @@
<?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
@@ -0,0 +1,44 @@
<?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]);
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* Session 鉴权中间件
* 所有接口(登录/找回密码除外)均需通过 requireLogin() / checkPermission() 校验。
* 写类接口还必须通过 checkAjax()(要求 X-Requested-With: XMLHttpRequest)。
*/
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/response.php';
/** 启动 Session */
function startSession()
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
/** 校验写类接口的 Ajax 头 */
function checkAjax()
{
if (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') !== 'XMLHttpRequest') {
Response::error('非法请求', 400);
}
}
/** 校验是否已登录,未登录返回 401 */
function requireLogin()
{
startSession();
if (empty($_SESSION['user_id'])) {
Response::error('未登录或会话已过期', 401);
}
}
/**
* 校验当前用户是否拥有某模块权限(同时校验登录态)
* @param string $module 菜单标识:dashboard/company/person/media/need/marketing/channel/document/preliminary/system/log
*/
function checkPermission($module)
{
requireLogin();
$perms = $_SESSION['permissions'] ?? [];
if (!in_array($module, $perms, true)) {
Response::error('无权操作', 403);
}
}
/** 仅超级管理员可操作(如操作日志查询) */
function requireSuperAdmin()
{
requireLogin();
if (($_SESSION['role_name'] ?? '') !== '超级管理员') {
Response::error('仅超级管理员可操作', 403);
}
}
/** 获取客户端 IP */
function clientIp()
{
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
/** 分页参数解析:page / limit */
function pageParams($defaultLimit = 20)
{
$page = max(1, (int)($_REQUEST['page'] ?? 1));
$limit = min(200, max(1, (int)($_REQUEST['limit'] ?? $defaultLimit)));
return [$page, $limit];
}
+43
View File
@@ -0,0 +1,43 @@
<?php
/**
* PDO 数据库连接类(单例模式)
* 用法:$pdo = DB::getInstance()->getPdo();
*/
class DB
{
private static $instance = null;
private $pdo;
private function __construct()
{
$cfg = require __DIR__ . '/../../config/database.php';
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
$cfg['host'],
$cfg['port'],
$cfg['dbname'],
$cfg['charset']
);
$this->pdo = new PDO($dsn, $cfg['username'], $cfg['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getPdo()
{
return $this->pdo;
}
// 禁止克隆
private function __clone() {}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* 通用辅助函数与字段白名单(供各模块 add/update/import 复用)
*/
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/response.php';
/** 企业表可写入字段白名单(key => 类型标记:s字符串/i整数/d日期/n数字) */
const COMPANY_FIELDS = [
'name_zh' => 's', 'name_en' => 's', 'display_name' => 's',
'business_role' => 's', 'country' => 's', 'registration_number' => 's',
'address' => 's', 'legal_form' => 's', 'legal_representative' => 's',
'business_scope' => 's', 'established_date' => 'd', 'registered_capital' => 's',
'industry' => 's', 'industry_subdivision' => 's',
'latest_employee_count' => 'i', 'latest_annual_revenue' => 's',
'is_listed' => 'i', 'stock_code' => 's', 'website' => 's',
];
/** 人员表可写入字段白名单 */
const PERSON_FIELDS = [
'union_id' => 's', 'full_name' => 's', 'gender' => 's', 'nationality' => 's',
'id_type' => 's', 'id_number' => 's', 'education' => 's', 'graduated_from' => 's',
'hometown' => 's', 'work_location' => 's',
];
/** 媒体(社交账号)表可写入字段白名单 */
const MEDIA_FIELDS = [
'owner_type' => 's', 'owner_id' => 'i', 'platform' => 's', 'account_id' => 's',
'profile_url' => 's', 'remark' => 's', 'is_primary' => 'i',
];
/** 媒体商业属性表可写入字段白名单 */
const MEDIA_ATTR_FIELDS = [
'account_level' => 's', 'content_categories' => 's', 'follower_count' => 'i',
'avg_read_count' => 'i', 'certification_type' => 's',
'special_requirements' => 's', 'media_remark' => 's',
];
/** 需求表可写入字段白名单 */
const NEED_FIELDS = [
'company_id' => 'i', 'contact_person' => 's', 'need_category' => 's',
'target_product_category' => 's', 'application_scenario' => 's', 'description' => 's',
];
/**
* 从 POST 中按白名单提取并清洗数据
* @param array $allowlist
* @return array
*/
function extractFields($allowlist)
{
$data = [];
foreach ($allowlist as $key => $type) {
$val = $_POST[$key] ?? null;
if ($val === null) {
continue;
}
$val = trim((string)$val);
if ($val === '') {
continue;
}
switch ($type) {
case 'i':
$data[$key] = (int)$val;
break;
case 'd':
$data[$key] = (strtotime($val) !== false) ? date('Y-m-d', strtotime($val)) : null;
break;
default:
$data[$key] = $val;
}
}
return $data;
}
/** 生成 INSERT 语句片段:字段名列表 + 占位符 */
function buildInsert($data)
{
$cols = array_keys($data);
$sql = '(`' . implode('`,`', $cols) . '`) VALUES (' . rtrim(str_repeat('?,', count($cols)), ',') . ')';
return [$sql, array_values($data)];
}
/** 生成 UPDATE SET 片段 */
function buildUpdate($data)
{
$sets = [];
foreach (array_keys($data) as $col) {
$sets[] = "`$col` = ?";
}
return [implode(',', $sets), array_values($data)];
}
+57
View File
@@ -0,0 +1,57 @@
<?php
/**
* 操作日志写入函数
* 所有写类接口(add/update/delete/import/export/audit/convert/login/logout)必须调用 logAction()。
*/
require_once __DIR__ . '/db.php';
/**
* 写入操作日志
* @param int|null $userId 操作人ID
* @param string|null $username 操作人账号
* @param string $action 操作类型
* @param string $module 操作模块
* @param string|null $targetTable 操作对象表名
* @param int|null $targetId 操作对象记录ID
* @param mixed $content 操作内容(数组,自动JSON编码)
* @return bool
*/
function logAction($userId, $username, $action, $module, $targetTable = null, $targetId = null, $content = null)
{
try {
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare(
"INSERT INTO system_logs (user_id, username, action, module, target_table, target_id, content, ip)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
);
$contentJson = ($content !== null && $content !== '')
? json_encode($content, JSON_UNESCAPED_UNICODE)
: null;
return $stmt->execute([$userId, $username, $action, $module, $targetTable, $targetId, $contentJson, $ip]);
} catch (Exception $e) {
// 日志失败不影响主流程
return false;
}
}
/**
* 基于当前 Session 用户写入日志(便捷函数)
* @param string $action
* @param string $module
* @param string|null $targetTable
* @param int|null $targetId
* @param mixed $content
*/
function logCurrent($action, $module, $targetTable = null, $targetId = null, $content = null)
{
return logAction(
$_SESSION['user_id'] ?? null,
$_SESSION['username'] ?? null,
$action,
$module,
$targetTable,
$targetId,
$content
);
}
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* 统一 JSON 返回类
* 用法:Response::success($data, $msg) / Response::error($msg, $code)
*/
class Response
{
/**
* 成功返回
* @param mixed $data
* @param string $msg
*/
public static function success($data = null, $msg = '操作成功')
{
self::json(['code' => 0, 'msg' => $msg, 'data' => $data]);
}
/**
* 失败返回
* @param string $msg
* @param int $code 非0错误码(401未登录/403无权限/400参数错误/1业务错误)
*/
public static function error($msg = '操作失败', $code = 1)
{
self::json(['code' => $code, 'msg' => $msg]);
}
private static function json($payload)
{
header('Content-Type: application/json; charset=utf-8');
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
exit;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
/**
* 企业新增接口 POST /api/company/add.php
* 必填:display_name;其余字段见 COMPANY_FIELDS 白名单
*/
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('company');
$data = extractFields(COMPANY_FIELDS);
// 显示名称不再单独录入:优先取中文名称,其次英文名称(数据库 display_name 为 NOT NULL)
if (empty($data['display_name'])) {
$data['display_name'] = $data['name_zh'] ?? ($data['name_en'] ?? '');
}
if (empty($data['display_name'])) {
Response::error('企业名称(中文名称或英文名称)为必填项', 400);
}
$pdo = DB::getInstance()->getPdo();
[$sql, $params] = buildInsert($data);
$pdo->prepare("INSERT INTO companies $sql")->execute($params);
$newId = (int)$pdo->lastInsertId();
logCurrent('add', 'company', 'companies', $newId, $data);
Response::success(['id' => $newId], '新增成功');
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* 企业删除接口(软删除) POST /api/company/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('company');
$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("UPDATE companies SET is_active = 0 WHERE id IN ($in)");
$stmt->execute($idList);
$affected = $stmt->rowCount();
logCurrent('delete', 'company', 'companies', null, ['ids' => $idList]);
Response::success(['affected' => $affected], "已删除 $affected 条记录");
+74
View File
@@ -0,0 +1,74 @@
<?php
/**
* 企业详情接口 GET /api/company/detail.php?id=1
* 返回企业主表信息 + 关联数据(产品品类/需求/文档/财务)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('company');
$id = (int)($_REQUEST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT * FROM companies WHERE id = ?");
$stmt->execute([$id]);
$company = $stmt->fetch();
if (!$company) {
Response::error('企业不存在');
}
$products = $pdo->prepare("SELECT id, category_name, category_description, is_core, is_active FROM company_products WHERE company_id = ? AND is_active = 1");
$products->execute([$id]);
$needs = $pdo->prepare("SELECT id, contact_person, need_category, target_product_category, application_scenario, description, is_valid, created_at FROM company_needs WHERE company_id = ? ORDER BY id DESC");
$needs->execute([$id]);
$docs = $pdo->prepare(
"SELECT cd.id, cd.doc_name, cd.storage_path, cd.file_type, cd.publish_date, cd.is_active
FROM company_documents cd
INNER JOIN document_links dl ON dl.document_id = cd.id
WHERE dl.owner_type = 'company' AND dl.owner_id = ? AND cd.is_active = 1"
);
$docs->execute([$id]);
$financials = $pdo->prepare("SELECT * FROM company_financials WHERE company_id = ? ORDER BY fiscal_year DESC");
$financials->execute([$id]);
$relations = $pdo->prepare(
"SELECT cr.id, cr.relation_type, cr.is_direct, cr.ownership_percentage, cr.established_date, c.display_name AS child_name
FROM company_relations cr
LEFT JOIN companies c ON c.id = cr.child_company_id
WHERE cr.parent_company_id = ?"
);
$relations->execute([$id]);
// 关联联系人(工作经历 + 联系方式)
$contacts = $pdo->prepare(
"SELECT p.id, p.full_name,
(SELECT GROUP_CONCAT(CONCAT_WS(' ', pwe2.position, pwe2.department) SEPARATOR ' / ')
FROM person_work_experiences pwe2
WHERE pwe2.company_id = ? AND pwe2.person_id = p.id AND pwe2.is_active = 1) AS position,
(SELECT GROUP_CONCAT(CONCAT(sa.platform, ':', sa.account_id) SEPARATOR ' | ')
FROM social_accounts sa WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.is_active = 1) AS contacts
FROM person_work_experiences pwe
LEFT JOIN persons p ON p.id = pwe.person_id
WHERE pwe.company_id = ? AND pwe.is_active = 1
GROUP BY p.id, p.full_name"
);
$contacts->execute([$id, $id]);
Response::success([
'company' => $company,
'products' => $products->fetchAll(),
'needs' => $needs->fetchAll(),
'documents' => $docs->fetchAll(),
'financials' => $financials->fetchAll(),
'relations' => $relations->fetchAll(),
'contacts' => $contacts->fetchAll(),
]);
+81
View File
@@ -0,0 +1,81 @@
<?php
/**
* 企业员工列表接口 GET /api/company/employees.php
* 入参:company_id(必填)/ page / limit / keyword(姓名)/ job_level(职级)
* 数据源:persons JOIN person_work_experiences(按公司),联系方式取 social_accounts
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('company');
$companyId = (int)($_REQUEST['company_id'] ?? 0);
if ($companyId <= 0) {
Response::error('参数错误', 400);
}
[$page, $limit] = pageParams(10);
$keyword = trim($_REQUEST['keyword'] ?? '');
$jobLevel = trim($_REQUEST['job_level'] ?? '');
$pdo = DB::getInstance()->getPdo();
// 公司存在性校验
$chk = $pdo->prepare("SELECT COUNT(*) FROM companies WHERE id = ?");
$chk->execute([$companyId]);
if ((int)$chk->fetchColumn() === 0) {
Response::error('企业不存在');
}
$where = ['pwe.company_id = ?', 'pwe.is_active = 1'];
$params = [$companyId];
if ($keyword !== '') {
$where[] = 'p.full_name LIKE ?';
$params[] = "%$keyword%";
}
if ($jobLevel !== '') {
$where[] = 'pwe.job_level = ?';
$params[] = $jobLevel;
}
$whereSql = implode(' AND ', $where);
// 总数
$stmt = $pdo->prepare(
"SELECT COUNT(*) FROM person_work_experiences pwe
INNER JOIN persons p ON p.id = pwe.person_id
WHERE $whereSql"
);
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
// 列表
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT p.id AS person_id, p.full_name, pwe.id AS work_id, pwe.position, pwe.department,
pwe.job_level, pwe.start_date, pwe.end_date, pwe.is_current,
(SELECT sa.account_id FROM social_accounts sa
WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.platform = 'phone' AND sa.is_active = 1
ORDER BY sa.is_primary DESC LIMIT 1) AS phone,
(SELECT sa.account_id FROM social_accounts sa
WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.platform = 'email' AND sa.is_active = 1
ORDER BY sa.is_primary DESC LIMIT 1) AS email
FROM person_work_experiences pwe
INNER JOIN persons p ON p.id = pwe.person_id
WHERE $whereSql
ORDER BY pwe.is_current DESC, pwe.start_date DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
// 职级字典(该公司范围内,供筛选下拉)
$lv = $pdo->prepare(
"SELECT DISTINCT job_level FROM person_work_experiences
WHERE company_id = ? AND is_active = 1 AND job_level IS NOT NULL AND job_level <> ''
ORDER BY job_level"
);
$lv->execute([$companyId]);
$levels = $lv->fetchAll(PDO::FETCH_COLUMN);
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit, 'levels' => $levels]);
+58
View File
@@ -0,0 +1,58 @@
<?php
/**
* 企业导出接口(CSV) GET/POST /api/company/export.php
* 入参:ids(必填,勾选的企业ID,逗号分隔)——只导出勾选的企业
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/logger.php';
checkPermission('company');
// 仅导出勾选的企业
$idsRaw = trim($_REQUEST['ids'] ?? '');
$idList = [];
if ($idsRaw !== '') {
foreach (explode(',', $idsRaw) as $v) {
$v = (int)trim($v);
if ($v > 0) {
$idList[] = $v;
}
}
}
$idList = array_unique($idList);
if (!$idList) {
Response::error('请先选择需要导出的企业数据', 1);
}
$pdo = DB::getInstance()->getPdo();
$in = implode(',', array_fill(0, count($idList), '?'));
$stmt = $pdo->prepare(
"SELECT id, name_zh, name_en, business_role, country, registration_number,
address, legal_representative, industry, industry_subdivision, website,
registered_capital, established_date, latest_employee_count, latest_annual_revenue,
is_listed, stock_code, created_at
FROM companies WHERE id IN ($in) AND is_active = 1 ORDER BY id DESC"
);
$stmt->execute($idList);
$list = $stmt->fetchAll();
logCurrent('export', 'company', 'companies', null, ['count' => count($list), 'ids' => $idList]);
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="companies_' . date('Ymd_His') . '.csv"');
echo "\xEF\xBB\xBF"; // UTF-8 BOM,便于 Excel 直接打开
$out = fopen('php://output', 'w');
fputcsv($out, ['ID', '公司名称', '英文名称', '业务角色', '国家/地区', '注册号', '地址', '法人', '行业', '行业细分', '官网', '注册资本', '成立日期', '员工数', '年营收', '是否上市', '股票代码', '创建时间']);
foreach ($list as $r) {
fputcsv($out, [
$r['id'], $r['name_zh'], $r['name_en'], $r['business_role'],
$r['country'], $r['registration_number'], $r['address'], $r['legal_representative'],
$r['industry'], $r['industry_subdivision'], $r['website'], $r['registered_capital'],
$r['established_date'], $r['latest_employee_count'], $r['latest_annual_revenue'],
$r['is_listed'], $r['stock_code'], date('Y-m-d', strtotime($r['created_at'])),
]);
}
fclose($out);
exit;
+84
View File
@@ -0,0 +1,84 @@
<?php
/**
* 企业 CSV 导入接口 POST /api/company/import.php (multipart/form-data, 字段名 file)
* CSV 表头:name_zh,name_en,display_name,business_role,country,registration_number,address,
* legal_form,legal_representative,industry,industry_subdivision,website,
* registered_capital,established_date,is_listed,stock_code
* 仅 display_name 为必填。
*/
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('company');
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
Response::error('请选择要上传的CSV文件', 400);
}
$tmp = $_FILES['file']['tmp_name'];
$handle = fopen($tmp, 'r');
if (!$handle) {
Response::error('无法读取文件', 400);
}
// 去掉 UTF-8 BOM
$first = fgets($handle);
$first = preg_replace('/^\xEF\xBB\xBF/', '', $first);
$header = str_getcsv(trim($first));
$map = [
'name_zh' => 'name_zh', 'name_en' => 'name_en', 'display_name' => 'display_name',
'business_role' => 'business_role', 'country' => 'country', 'registration_number' => 'registration_number',
'address' => 'address', 'legal_form' => 'legal_form', 'legal_representative' => 'legal_representative',
'industry' => 'industry', 'industry_subdivision' => 'industry_subdivision', 'website' => 'website',
'registered_capital' => 'registered_capital', 'established_date' => 'established_date',
'is_listed' => 'is_listed', 'stock_code' => 'stock_code',
];
$pdo = DB::getInstance()->getPdo();
$inserted = 0;
$failed = 0;
$stmt = $pdo->prepare(
"INSERT INTO companies
(name_zh, name_en, display_name, business_role, country, registration_number,
address, legal_form, legal_representative, industry, industry_subdivision, website,
registered_capital, established_date, is_listed, stock_code)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
);
while (($row = fgetcsv($handle)) !== false) {
$row = array_map('trim', $row);
$rec = [];
foreach ($header as $idx => $col) {
$col = trim($col);
if (isset($map[$col]) && isset($row[$idx])) {
$rec[$map[$col]] = $row[$idx];
}
}
if (empty($rec['display_name'])) {
$failed++;
continue;
}
$rec['established_date'] = ($rec['established_date'] ?? '') !== '' ? date('Y-m-d', strtotime($rec['established_date'])) : null;
try {
$stmt->execute([
$rec['name_zh'] ?? null, $rec['name_en'] ?? null, $rec['display_name'],
$rec['business_role'] ?? null, $rec['country'] ?? null, $rec['registration_number'] ?? null,
$rec['address'] ?? null, $rec['legal_form'] ?? null, $rec['legal_representative'] ?? null,
$rec['industry'] ?? null, $rec['industry_subdivision'] ?? null, $rec['website'] ?? null,
$rec['registered_capital'] ?? null, $rec['established_date'],
($rec['is_listed'] ?? 0) ? 1 : 0, $rec['stock_code'] ?? null,
]);
$inserted++;
} catch (Exception $e) {
$failed++;
}
}
fclose($handle);
logCurrent('import', 'company', 'companies', null, ['inserted' => $inserted, 'failed' => $failed]);
Response::success(['inserted' => $inserted, 'failed' => $failed], "导入完成:成功 $inserted 条,失败 $failed 条");
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
* 企业列表接口 GET/POST /api/company/list.php
* 参数:page / limit / keyword(公司名/注册号)/ industry / business_role / country / is_active
* 返回:{ list, total, page, limit }
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('company');
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
$industry = trim($_REQUEST['industry'] ?? '');
$role = trim($_REQUEST['business_role'] ?? '');
$country = trim($_REQUEST['country'] ?? '');
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
$where = ['is_active = 1'];
$params = [];
if ($active !== null) {
$where = [];
$params = [];
$where[] = 'is_active = ?';
$params[] = $active;
}
if ($keyword !== '') {
$where[] = '(display_name LIKE ? OR name_zh LIKE ? OR name_en LIKE ? OR registration_number LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like, $like, $like);
}
if ($industry !== '') {
$where[] = 'industry = ?';
$params[] = $industry;
}
if ($role !== '') {
$where[] = 'business_role = ?';
$params[] = $role;
}
if ($country !== '') {
$where[] = 'country = ?';
$params[] = $country;
}
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM companies WHERE $whereSql");
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, name_zh, name_en, display_name, business_role, country, registration_number,
address, legal_representative, industry, industry_subdivision, website,
latest_employee_count, latest_annual_revenue, is_listed, stock_code,
is_active, created_at, updated_at
FROM companies
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]);
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* 企业编辑接口 POST /api/company/update.php
* 入参:id + 需要更新的白名单字段
*/
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('company');
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$data = extractFields(COMPANY_FIELDS);
unset($data['display_name']); // 显示名称不允许通过编辑接口置空/改名,如需改名请走完整字段
if (empty($data)) {
Response::error('没有需要更新的字段', 400);
}
$pdo = DB::getInstance()->getPdo();
$check = $pdo->prepare("SELECT id, display_name FROM companies WHERE id = ?");
$check->execute([$id]);
$old = $check->fetch();
if (!$old) {
Response::error('企业不存在');
}
[$sets, $params] = buildUpdate($data);
$params[] = $id;
$pdo->prepare("UPDATE companies SET $sets WHERE id = ?")->execute($params);
logCurrent('update', 'company', 'companies', $id, ['before' => $old, 'after' => $data]);
Response::success(null, '更新成功');
+194
View File
@@ -0,0 +1,194 @@
<?php
/**
* Dashboard 首页数据接口 GET /api/dashboard/stats.php
* 返回:8 个指标卡 + 区域分布(国外=世界地图按国家 / 国内=中国地图按省份,企业/人员双维度)
* + 行业分布(企业/人员)+ 需求关键词(简化)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('dashboard');
$pdo = DB::getInstance()->getPdo();
// ---------- 8 个指标卡 ----------
$sqlMetrics = [
'company_total' => "SELECT COUNT(*) AS c FROM companies WHERE is_active = 1",
'person_total' => "SELECT COUNT(*) AS c FROM persons WHERE is_active = 1",
'company_new' => "SELECT COUNT(*) AS c FROM companies WHERE is_active = 1 AND DATE(created_at) = CURDATE() - INTERVAL 1 DAY",
'person_new' => "SELECT COUNT(*) AS c FROM persons WHERE is_active = 1 AND DATE(created_at) = CURDATE() - INTERVAL 1 DAY",
'preliminary' => "SELECT COUNT(*) AS c FROM preliminary_data WHERE status = '待处理' AND is_active = 1",
'industry_count' => "SELECT COUNT(DISTINCT industry) AS c FROM companies WHERE industry IS NOT NULL AND industry <> ''",
'manufacturer' => "SELECT COUNT(*) AS c FROM companies WHERE business_role = 'manufacturer' AND is_active = 1",
'integrator' => "SELECT COUNT(*) AS c FROM companies WHERE business_role IN ('integrator','distributor') AND is_active = 1",
'media_resource' => "SELECT COUNT(*) AS c FROM media_commercial_attributes",
];
$metrics = [];
foreach ($sqlMetrics as $key => $sql) {
$metrics[$key] = (int)$pdo->query($sql)->fetchColumn();
}
// ---------- 区域分布 ----------
// 国外(世界地图):企业按 country,人员按 nationality
$regionWorldCompany = $pdo->query(
"SELECT country AS name, COUNT(*) AS value
FROM companies
WHERE country IS NOT NULL AND country <> '' AND is_active = 1
GROUP BY country ORDER BY value DESC LIMIT 60"
)->fetchAll();
$regionWorldPerson = $pdo->query(
"SELECT nationality AS name, COUNT(*) AS value
FROM persons
WHERE nationality IS NOT NULL AND nationality <> '' AND is_active = 1
GROUP BY nationality ORDER BY value DESC LIMIT 60"
)->fetchAll();
if (!$regionWorldPerson) {
$regionWorldPerson = $pdo->query(
"SELECT work_location AS name, COUNT(*) AS value
FROM persons WHERE work_location IS NOT NULL AND work_location <> '' AND is_active = 1
GROUP BY work_location ORDER BY value DESC LIMIT 60"
)->fetchAll();
}
// 国内(中国地图):按省份统计(企业取注册地址/国家;人员取工作所在地/家乡)
$regionChinaCompany = provinceCount(
$pdo->query("SELECT country, address FROM companies WHERE is_active = 1")->fetchAll(),
['country', 'address']
);
$regionChinaPerson = provinceCount(
$pdo->query("SELECT work_location, hometown FROM persons WHERE is_active = 1")->fetchAll(),
['work_location', 'hometown']
);
// ---------- 行业分布(企业直接取 industry;人员经工作经历关联到企业行业) ----------
$industryCompany = $pdo->query(
"SELECT industry AS name, COUNT(*) AS value
FROM companies
WHERE industry IS NOT NULL AND industry <> '' AND is_active = 1
GROUP BY industry ORDER BY value DESC LIMIT 20"
)->fetchAll();
$industryPerson = $pdo->query(
"SELECT c.industry AS name, COUNT(DISTINCT pwe.person_id) AS value
FROM person_work_experiences pwe
LEFT JOIN companies c ON c.id = pwe.company_id
WHERE pwe.is_active = 1 AND c.industry IS NOT NULL AND c.industry <> '' AND c.is_active = 1
GROUP BY c.industry ORDER BY value DESC LIMIT 20"
)->fetchAll();
// ---------- 需求关键词(简化:从 company_needs.description 提取高频词,占位) ----------
$needRows = $pdo->query(
"SELECT description FROM company_needs WHERE is_valid = 1 AND description IS NOT NULL AND description <> '' LIMIT 200"
)->fetchAll();
$keywordMap = [];
foreach ($needRows as $row) {
$parts = preg_split('/[,。、,.;;::\s\/\|\-\+\/]+/u', $row['description']);
foreach ($parts as $part) {
$part = trim($part);
if (mb_strlen($part, 'utf-8') >= 2 && mb_strlen($part, 'utf-8') <= 12) {
$keywordMap[$part] = ($keywordMap[$part] ?? 0) + 1;
}
}
}
arsort($keywordMap);
$wordCloud = [];
$i = 0;
foreach ($keywordMap as $word => $count) {
if ($i++ >= 50) break;
$wordCloud[] = ['name' => $word, 'value' => $count];
}
Response::success([
'metrics' => $metrics,
'region' => [
'company' => ['world' => $regionWorldCompany, 'china' => $regionChinaCompany],
'person' => ['world' => $regionWorldPerson, 'china' => $regionChinaPerson],
],
'industry' => ['company' => $industryCompany, 'person' => $industryPerson],
'wordcloud' => $wordCloud,
]);
/**
* 从一组记录的文本字段中统计中国省份分布
* @param array $rows 记录列表(关联数组)
* @param array $fields 用于匹配的字段名
* @return array [{name:省份, value:数量}]
*/
function provinceCount($rows, $fields)
{
// 34 个省级行政区(含直辖市/自治区/特别行政区),匹配顺序:长名称优先
$provinces = [
'内蒙古自治区', '广西壮族自治区', '西藏自治区', '宁夏回族自治区', '新疆维吾尔自治区',
'黑龙江省', '河北省', '山西省', '辽宁省', '吉林省', '江苏省', '浙江省', '安徽省',
'福建省', '江西省', '山东省', '河南省', '湖北省', '湖南省', '广东省', '海南省',
'四川省', '贵州省', '云南省', '陕西省', '甘肃省', '青海省',
'北京', '天津', '上海', '重庆', '台湾', '香港', '澳门',
];
// 常见城市 -> 省份(地址/工作地只有城市名时使用)
$city2prov = [
'深圳' => '广东', '广州' => '广东', '东莞' => '广东', '佛山' => '广东',
'珠海' => '广东', '中山' => '广东', '惠州' => '广东', '汕头' => '广东', '江门' => '广东',
'杭州' => '浙江', '宁波' => '浙江', '温州' => '浙江', '嘉兴' => '浙江', '绍兴' => '浙江',
'南京' => '江苏', '苏州' => '江苏', '无锡' => '江苏', '常州' => '江苏', '南通' => '江苏',
'成都' => '四川', '绵阳' => '四川', '武汉' => '湖北', '宜昌' => '湖北', '长沙' => '湖南',
'郑州' => '河南', '洛阳' => '河南', '西安' => '陕西', '青岛' => '山东', '济南' => '山东',
'烟台' => '山东', '潍坊' => '山东', '大连' => '辽宁', '沈阳' => '辽宁', '哈尔滨' => '黑龙江',
'长春' => '吉林', '合肥' => '安徽', '芜湖' => '安徽', '福州' => '福建', '厦门' => '福建',
'泉州' => '福建', '昆明' => '云南', '贵阳' => '贵州', '南昌' => '江西', '石家庄' => '河北',
'太原' => '山西', '兰州' => '甘肃', '银川' => '宁夏', '西宁' => '青海', '乌鲁木齐' => '新疆',
'呼和浩特' => '内蒙古', '南宁' => '广西', '海口' => '海南', '三亚' => '海南',
'拉萨' => '西藏', '台北' => '台湾', '高雄' => '台湾', '香港' => '香港', '澳门' => '澳门',
];
$counts = [];
foreach ($rows as $row) {
$text = '';
foreach ($fields as $f) {
if (!empty($row[$f])) {
$text .= $row[$f] . ' ';
}
}
$province = null;
foreach ($provinces as $p) {
if (mb_strpos($text, $p) !== false) {
$province = $p;
break;
}
}
if ($province === null) {
foreach ($city2prov as $city => $p) {
if (mb_strpos($text, $city) !== false) {
$province = $p;
break;
}
}
}
// 长名 -> 短名(与中国地图 echarts china.js 的省份名保持一致)
if ($province !== null) {
$shortMap = [
'内蒙古自治区' => '内蒙古', '广西壮族自治区' => '广西', '西藏自治区' => '西藏',
'宁夏回族自治区' => '宁夏', '新疆维吾尔自治区' => '新疆',
'黑龙江省' => '黑龙江', '河北省' => '河北', '山西省' => '山西', '辽宁省' => '辽宁',
'吉林省' => '吉林', '江苏省' => '江苏', '浙江省' => '浙江', '安徽省' => '安徽',
'福建省' => '福建', '江西省' => '江西', '山东省' => '山东', '河南省' => '河南',
'湖北省' => '湖北', '湖南省' => '湖南', '广东省' => '广东', '海南省' => '海南',
'四川省' => '四川', '贵州省' => '贵州', '云南省' => '云南', '陕西省' => '陕西',
'甘肃省' => '甘肃', '青海省' => '青海', '台湾省' => '台湾',
];
if (isset($shortMap[$province])) {
$province = $shortMap[$province];
}
}
if ($province !== null) {
$counts[$province] = ($counts[$province] ?? 0) + 1;
}
}
arsort($counts);
$result = [];
foreach ($counts as $name => $value) {
$result[] = ['name' => $name, 'value' => $value];
}
return $result;
}
+54
View File
@@ -0,0 +1,54 @@
<?php
/**
* 文件删除接口(同时删除物理文件) POST /api/document/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('document');
$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("SELECT id, storage_path FROM company_documents WHERE id IN ($in)");
$stmt->execute($idList);
$docs = $stmt->fetchAll();
// 删除物理文件(仅限本站上传目录内的文件)
foreach ($docs as $doc) {
$path = $doc['storage_path'];
if (strpos($path, 'static/uploads/documents/') === 0) {
$full = __DIR__ . '/../../' . $path;
if (is_file($full)) {
@unlink($full);
}
}
}
// 删除记录(document_links 级联删除)
$del = $pdo->prepare("DELETE FROM company_documents WHERE id IN ($in)");
$del->execute($idList);
$affected = $del->rowCount();
logCurrent('delete', 'document', 'company_documents', null, ['ids' => $idList]);
Response::success(['affected' => $affected], "已删除 $affected 个文件(含物理文件)");
+49
View File
@@ -0,0 +1,49 @@
<?php
/**
* 文件列表接口 GET/POST /api/document/list.php
* 参数:page / limit / keyword / file_type / is_active
* 数据源:company_documents 表
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('document');
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
$fileType = trim($_REQUEST['file_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[] = '(doc_name LIKE ? OR document_source LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like);
}
if ($fileType !== '') { $where[] = 'file_type = ?'; $params[] = $fileType; }
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM company_documents WHERE $whereSql");
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, doc_name, storage_path, file_type, is_current, publish_date, document_source, tags, is_active, created_at
FROM company_documents
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]);
+75
View File
@@ -0,0 +1,75 @@
<?php
/**
* 文件上传接口 POST /api/document/upload.php (multipart/form-data, 字段名 file)
* 可选:doc_name(默认取原文件名)/ tags(JSON)
* 存储:static/uploads/documents/年/月/随机文件名
* 返回:相对路径 storage_path
*/
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('document');
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
Response::error('请选择要上传的文件', 400);
}
$file = $_FILES['file'];
$maxSize = 50 * 1024 * 1024; // 50MB
if ($file['size'] > $maxSize) {
Response::error('文件不能超过50MB', 400);
}
// 安全扩展名白名单
$extMap = [
'pdf' => 'application/pdf', 'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ppt' => 'application/vnd.ms-powerpoint',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'txt' => 'text/plain', 'csv' => 'text/csv', 'png' => 'image/png',
'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif',
'zip' => 'application/zip', 'rar' => 'application/x-rar-compressed',
];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!isset($extMap[$ext])) {
Response::error('不允许的文件类型:' . ($ext !== '' ? $ext : '无扩展名'), 400);
}
// 目录:static/uploads/documents/年/月
$baseDir = __DIR__ . '/../../static/uploads/documents';
$subDir = date('Y') . '/' . date('m');
$dir = $baseDir . '/' . $subDir;
if (!is_dir($dir) && !mkdir($dir, 0777, true)) {
Response::error('创建上传目录失败', 500);
}
$newName = date('YmdHis') . '_' . substr(uniqid(), -6) . '.' . $ext;
$dest = $dir . '/' . $newName;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
Response::error('文件保存失败', 500);
}
$storagePath = 'static/uploads/documents/' . $subDir . '/' . $newName;
$docName = trim($_POST['doc_name'] ?? '') !== '' ? trim($_POST['doc_name']) : $file['name'];
$tags = $_POST['tags'] ?? null;
$tagsJson = null;
if ($tags !== null && $tags !== '') {
$decoded = json_decode($tags, true);
$tagsJson = is_array($decoded) ? json_encode($decoded, JSON_UNESCAPED_UNICODE) : null;
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare(
"INSERT INTO company_documents (doc_name, storage_path, file_type, is_current, document_source, tags)
VALUES (?, ?, ?, 1, ?, ?)"
);
$stmt->execute([$docName, $storagePath, $extMap[$ext], '本地上传', $tagsJson]);
$newId = (int)$pdo->lastInsertId();
logCurrent('upload', 'document', 'company_documents', $newId, ['doc_name' => $docName, 'storage_path' => $storagePath]);
Response::success(['id' => $newId, 'storage_path' => $storagePath, 'doc_name' => $docName], '上传成功');
+29
View File
@@ -0,0 +1,29 @@
<?php
/**
* 数据定制接口(占位) GET/POST /api/marketing/data_custom.php
* 后续扩展:定制数据需求提交、报价、交付等。
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/logger.php';
checkPermission('marketing');
$action = $_REQUEST['action'] ?? 'info';
if ($action === 'submit' && $_SERVER['REQUEST_METHOD'] === 'POST') {
checkAjax();
$content = [
'contact_name' => trim($_POST['contact_name'] ?? ''),
'contact_phone' => trim($_POST['contact_phone'] ?? ''),
'requirements' => trim($_POST['requirements'] ?? ''),
];
logCurrent('audit', 'marketing', null, null, $content);
Response::success(null, '需求已提交(占位实现,后续接入业务流)');
}
Response::success([
'title' => '数据定制',
'description' => '按行业/区域/规模等维度定制企业数据、人员数据,功能建设中(占位)。',
'status' => 'placeholder',
]);
+114
View File
@@ -0,0 +1,114 @@
<?php
/**
* EDM 筛选接口 GET/POST /api/marketing/edm.php
* action:
* industries 返回行业下拉数据
* regions 返回区域二级联动数据(省/州 -> 市/区,来自 persons.work_location / hometown)
* filter 按 行业 + 区域 + 关键词 筛选邮箱列表(persons.email + companies 关联联系人邮箱)
* 入参(filter):industry / province / city / keyword / page / limit
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('marketing');
$action = $_REQUEST['action'] ?? 'filter';
$pdo = DB::getInstance()->getPdo();
// ---------- 行业下拉 ----------
if ($action === 'industries') {
$rows = $pdo->query(
"SELECT DISTINCT industry FROM companies WHERE industry IS NOT NULL AND industry <> '' AND is_active = 1 ORDER BY industry"
)->fetchAll(PDO::FETCH_COLUMN);
Response::success(['industries' => $rows]);
}
// ---------- 区域二级联动 ----------
if ($action === 'regions') {
// 从 persons.work_location 与 hometown 提取 省/州 与 市/区
$rows = $pdo->query(
"SELECT work_location AS loc FROM persons WHERE work_location IS NOT NULL AND work_location <> '' AND is_active = 1
UNION SELECT hometown AS loc FROM persons WHERE hometown IS NOT NULL AND hometown <> '' AND is_active = 1"
)->fetchAll(PDO::FETCH_COLUMN);
$regions = [];
foreach ($rows as $loc) {
$parts = preg_split('/[\s\-—–\/,,]+/u', trim($loc));
$province = $parts[0] ?? $loc;
$city = isset($parts[1]) && $parts[1] !== '' ? $parts[1] : '';
if ($province === '') continue;
if (!isset($regions[$province])) {
$regions[$province] = [];
}
if ($city !== '' && !in_array($city, $regions[$province], true)) {
$regions[$province][] = $city;
}
}
Response::success(['regions' => $regions]);
}
// ---------- 筛选邮箱列表 ----------
if ($action === 'filter') {
$industry = trim($_REQUEST['industry'] ?? '');
$province = trim($_REQUEST['province'] ?? '');
$city = trim($_REQUEST['city'] ?? '');
$keyword = trim($_REQUEST['keyword'] ?? '');
// 邮箱来源:persons 的 social_accounts(platform=email),关联工作经历(company) 做行业过滤
$where = ["sa.platform = 'email'", "sa.is_active = 1", 'p.is_active = 1'];
$params = [];
if ($industry !== '') {
$where[] = "EXISTS (
SELECT 1 FROM person_work_experiences pwe
LEFT JOIN companies c ON c.id = pwe.company_id
WHERE pwe.person_id = p.id AND pwe.is_active = 1 AND c.industry = ?
)";
$params[] = $industry;
}
if ($province !== '') {
$where[] = "(p.work_location LIKE ? OR p.hometown LIKE ?)";
$params[] = "%$province%";
$params[] = "%$province%";
}
if ($city !== '') {
$where[] = "(p.work_location LIKE ? OR p.hometown LIKE ?)";
$params[] = "%$city%";
$params[] = "%$city%";
}
if ($keyword !== '') {
$where[] = '(p.full_name LIKE ? OR sa.account_id LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like);
}
$whereSql = implode(' AND ', $where);
$stmt = $pdo->prepare(
"SELECT COUNT(*) FROM social_accounts sa
LEFT JOIN persons p ON p.id = sa.owner_id
WHERE $whereSql"
);
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
[$page, $limit] = pageParams(20);
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT sa.id, sa.account_id AS email, p.id AS person_id, p.full_name, p.work_location, p.hometown,
(SELECT c.display_name FROM person_work_experiences pwe
LEFT JOIN companies c ON c.id = pwe.company_id
WHERE pwe.person_id = p.id AND pwe.is_current = 1 LIMIT 1) AS company_name
FROM social_accounts sa
LEFT JOIN persons p ON p.id = sa.owner_id
WHERE $whereSql
ORDER BY sa.id DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
}
Response::error('未知操作', 400);
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* EDM 邮箱列表导出接口(CSV) GET/POST /api/marketing/edm_export.php
* 入参同 edm.php filter:industry / province / city / keyword
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/logger.php';
checkPermission('marketing');
// 仅导出勾选的邮箱(emails 逗号分隔)
$emailsRaw = trim($_REQUEST['emails'] ?? '');
$emailList = [];
if ($emailsRaw !== '') {
foreach (explode(',', $emailsRaw) as $v) {
$v = trim($v);
if ($v !== '') {
$emailList[] = $v;
}
}
}
$emailList = array_unique($emailList);
if (!$emailList) {
Response::error('请先选择需要导出的邮箱', 1);
}
$in = implode(',', array_fill(0, count($emailList), '?'));
$where = ["sa.platform = 'email'", 'sa.is_active = 1', 'p.is_active = 1', "sa.account_id IN ($in)"];
$params = $emailList;
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare(
"SELECT sa.account_id AS email, p.full_name, p.work_location, p.hometown
FROM social_accounts sa
LEFT JOIN persons p ON p.id = sa.owner_id
WHERE $whereSql
ORDER BY sa.id DESC"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
logCurrent('export', 'marketing', 'social_accounts', null, ['count' => count($list), 'emails' => $emailList]);
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="edm_emails_' . date('Ymd_His') . '.csv"');
echo "\xEF\xBB\xBF";
$out = fopen('php://output', 'w');
fputcsv($out, ['邮箱', '姓名', '工作所在地', '家乡']);
foreach ($list as $r) {
fputcsv($out, [$r['email'], $r['full_name'], $r['work_location'], $r['hometown']]);
}
fclose($out);
exit;
+28
View File
@@ -0,0 +1,28 @@
<?php
/**
* 媒体批发接口(占位) GET/POST /api/marketing/media_wholesale.php
* 后续扩展:媒体资源批量询价、下单等。
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/logger.php';
checkPermission('marketing');
$action = $_REQUEST['action'] ?? 'info';
if ($action === 'inquiry' && $_SERVER['REQUEST_METHOD'] === 'POST') {
checkAjax();
$content = [
'media_ids' => trim($_POST['media_ids'] ?? ''),
'remark' => trim($_POST['remark'] ?? ''),
];
logCurrent('audit', 'marketing', 'social_accounts', null, $content);
Response::success(null, '询价已提交(占位实现,后续接入业务流)');
}
Response::success([
'title' => '媒体批发',
'description' => '媒体账号资源批量采购/询价,功能建设中(占位)。',
'status' => 'placeholder',
]);
+57
View File
@@ -0,0 +1,57 @@
<?php
/**
* 媒体数据新增接口 POST /api/media/add.php
* 必填:owner_type / owner_id / platform / account_id
* 可选:profile_url / remark / is_primary + 商业属性(account_level/certification_type/follower_count 等)
*/
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('media');
$data = extractFields(MEDIA_FIELDS);
if (!in_array($data['owner_type'] ?? '', ['person', 'company'], true)) {
Response::error('owner_type 必须为 person 或 company', 400);
}
if (empty($data['platform']) || empty($data['account_id'])) {
Response::error('平台(platform)和账号(account_id)为必填项', 400);
}
$pdo = DB::getInstance()->getPdo();
// owner 存在性校验
if ($data['owner_type'] === 'person') {
$chk = $pdo->prepare("SELECT COUNT(*) FROM persons WHERE id = ?");
} else {
$chk = $pdo->prepare("SELECT COUNT(*) FROM companies WHERE id = ?");
}
$chk->execute([$data['owner_id']]);
if ((int)$chk->fetchColumn() === 0) {
Response::error('归属对象不存在', 400);
}
// platform + account_id 唯一
$chk = $pdo->prepare("SELECT COUNT(*) FROM social_accounts WHERE platform = ? AND account_id = ?");
$chk->execute([$data['platform'], $data['account_id']]);
if ((int)$chk->fetchColumn() > 0) {
Response::error('该平台账号已存在', 400);
}
[$sql, $params] = buildInsert($data);
$pdo->prepare("INSERT INTO social_accounts $sql")->execute($params);
$newId = (int)$pdo->lastInsertId();
// 写入商业属性
$attr = extractFields(MEDIA_ATTR_FIELDS);
if (!empty($attr)) {
$attr['social_account_id'] = $newId;
[$attrSql, $attrParams] = buildInsert($attr);
$pdo->prepare("INSERT INTO media_commercial_attributes $attrSql")->execute($attrParams);
}
logCurrent('add', 'media', 'social_accounts', $newId, ['data' => $data, 'attr' => $attr]);
Response::success(['id' => $newId], '新增成功');
+36
View File
@@ -0,0 +1,36 @@
<?php
/**
* 媒体数据删除接口(软删除) POST /api/media/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('media');
$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("UPDATE social_accounts SET is_active = 0 WHERE id IN ($in)");
$stmt->execute($idList);
$affected = $stmt->rowCount();
logCurrent('delete', 'media', 'social_accounts', null, ['ids' => $idList]);
Response::success(['affected' => $affected], "已删除 $affected 条记录");
+30
View File
@@ -0,0 +1,30 @@
<?php
/**
* 媒体数据详情接口 GET /api/media/detail.php?id=1
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('media');
$id = (int)($_REQUEST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare(
"SELECT sa.*, mca.account_level, mca.content_categories, mca.follower_count,
mca.avg_read_count, mca.certification_type, mca.special_requirements, mca.media_remark
FROM social_accounts sa
LEFT JOIN media_commercial_attributes mca ON mca.social_account_id = sa.id
WHERE sa.id = ?"
);
$stmt->execute([$id]);
$row = $stmt->fetch();
if (!$row) {
Response::error('媒体账号不存在');
}
Response::success(['media' => $row]);
+65
View File
@@ -0,0 +1,65 @@
<?php
/**
* 媒体数据导出接口(CSV) GET/POST /api/media/export.php
* 参数同 list.php
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/logger.php';
checkPermission('media');
// 仅导出勾选的媒体账号
$idsRaw = trim($_REQUEST['ids'] ?? '');
$idList = [];
if ($idsRaw !== '') {
foreach (explode(',', $idsRaw) as $v) {
$v = (int)trim($v);
if ($v > 0) {
$idList[] = $v;
}
}
}
$idList = array_unique($idList);
if (!$idList) {
Response::error('请先选择需要导出的媒体数据', 1);
}
$where = ['sa.id IN (' . implode(',', array_fill(0, count($idList), '?')) . ')', 'sa.is_active = 1'];
$params = $idList;
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare(
"SELECT sa.id, sa.owner_type, sa.owner_id, sa.platform, sa.account_id, sa.profile_url, sa.remark,
mca.account_level, mca.content_categories, mca.follower_count, mca.avg_read_count,
mca.certification_type, sa.created_at,
CASE sa.owner_type
WHEN 'person' THEN (SELECT p.full_name FROM persons p WHERE p.id = sa.owner_id)
WHEN 'company' THEN (SELECT c.display_name FROM companies c WHERE c.id = sa.owner_id)
ELSE NULL END AS owner_name
FROM social_accounts sa
LEFT JOIN media_commercial_attributes mca ON mca.social_account_id = sa.id
WHERE $whereSql ORDER BY sa.id DESC"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
logCurrent('export', 'media', 'social_accounts', null, ['count' => count($list)]);
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="media_' . date('Ymd_His') . '.csv"');
echo "\xEF\xBB\xBF";
$out = fopen('php://output', 'w');
fputcsv($out, ['ID', '归属类型', '归属ID', '归属名称', '平台', '账号', '主页链接', '备注', '等级', '内容领域', '粉丝数', '平均阅读', '认证类型', '创建时间']);
foreach ($list as $r) {
fputcsv($out, [
$r['id'], $r['owner_type'], $r['owner_id'], $r['owner_name'], $r['platform'],
$r['account_id'], $r['profile_url'], $r['remark'], $r['account_level'],
$r['content_categories'], $r['follower_count'], $r['avg_read_count'],
$r['certification_type'], $r['created_at'],
]);
}
fclose($out);
exit;
+95
View File
@@ -0,0 +1,95 @@
<?php
/**
* 媒体数据 CSV 导入接口 POST /api/media/import.php (multipart/form-data, 字段名 file)
* CSV 表头:owner_type,owner_id,platform,account_id,profile_url,remark,is_primary,
* account_level,content_categories,follower_count,avg_read_count,certification_type
* owner_type 为 person/company 时 owner_id 也可填对应名称(display_name/full_name),自动解析为ID。
*/
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('media');
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
Response::error('请选择要上传的CSV文件', 400);
}
$handle = fopen($_FILES['file']['tmp_name'], 'r');
if (!$handle) {
Response::error('无法读取文件', 400);
}
$first = preg_replace('/^\xEF\xBB\xBF/', '', fgets($handle));
$header = str_getcsv(trim($first));
$pdo = DB::getInstance()->getPdo();
$insAccount = $pdo->prepare(
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary, is_active)
VALUES (?,?,?,?,?,?,?,1)"
);
$insAttr = $pdo->prepare(
"INSERT INTO media_commercial_attributes
(social_account_id, account_level, content_categories, follower_count, avg_read_count, certification_type)
VALUES (?,?,?,?,?,?)"
);
$inserted = 0;
$failed = 0;
while (($row = fgetcsv($handle)) !== false) {
$row = array_map('trim', $row);
$rec = [];
foreach ($header as $idx => $col) {
$col = trim($col);
if (isset($row[$idx])) {
$rec[$col] = $row[$idx];
}
}
if (empty($rec['platform']) || empty($rec['account_id']) || empty($rec['owner_type'])) {
$failed++;
continue;
}
$ownerType = $rec['owner_type'] === 'company' ? 'company' : 'person';
$ownerId = (int)($rec['owner_id'] ?? 0);
if ($ownerId <= 0) {
// 按名称解析
$nameCol = $ownerType === 'company' ? 'display_name' : 'full_name';
$tbl = $ownerType === 'company' ? 'companies' : 'persons';
$find = $pdo->prepare("SELECT id FROM $tbl WHERE $nameCol = ? LIMIT 1");
$find->execute([$rec['owner_name'] ?? $rec['owner_id'] ?? '']);
$ownerId = (int)$find->fetchColumn();
}
if ($ownerId <= 0) {
$failed++;
continue;
}
try {
$chk = $pdo->prepare("SELECT COUNT(*) FROM social_accounts WHERE platform = ? AND account_id = ?");
$chk->execute([$rec['platform'], $rec['account_id']]);
if ((int)$chk->fetchColumn() > 0) {
$failed++;
continue;
}
$insAccount->execute([
$ownerType, $ownerId, $rec['platform'], $rec['account_id'],
$rec['profile_url'] ?? null, $rec['remark'] ?? null, !empty($rec['is_primary']) ? 1 : 0,
]);
$newId = (int)$pdo->lastInsertId();
if (!empty($rec['account_level']) || !empty($rec['certification_type'])) {
$insAttr->execute([
$newId, $rec['account_level'] ?? null, $rec['content_categories'] ?? null,
(int)($rec['follower_count'] ?? 0), (int)($rec['avg_read_count'] ?? 0),
$rec['certification_type'] ?? null,
]);
}
$inserted++;
} catch (Exception $e) {
$failed++;
}
}
fclose($handle);
logCurrent('import', 'media', 'social_accounts', null, ['inserted' => $inserted, 'failed' => $failed]);
Response::success(['inserted' => $inserted, 'failed' => $failed], "导入完成:成功 $inserted 条,失败 $failed 条");
+77
View File
@@ -0,0 +1,77 @@
<?php
/**
* 媒体数据列表接口 GET/POST /api/media/list.php
* 参数:page / limit / keyword / platform / certification_type / account_level / owner_type / is_active
* 数据源:social_accounts JOIN media_commercial_attributes
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('media');
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
$platform = trim($_REQUEST['platform'] ?? '');
$certType = trim($_REQUEST['certification_type'] ?? '');
$level = trim($_REQUEST['account_level'] ?? '');
$ownerType = trim($_REQUEST['owner_type'] ?? '');
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
$where = ['sa.is_active = 1'];
$params = [];
if ($active !== null) {
$where = ['sa.is_active = ?'];
$params = [$active];
}
if ($keyword !== '') {
$where[] = "(sa.account_id LIKE ? OR sa.profile_url LIKE ? OR sa.owner_id IN (
SELECT p.id FROM persons p WHERE p.full_name LIKE ?
UNION SELECT c.id FROM companies c WHERE c.display_name LIKE ?
))";
$like = "%$keyword%";
array_push($params, $like, $like, $like, $like);
}
if ($platform !== '') { $where[] = 'sa.platform = ?'; $params[] = $platform; }
if ($certType !== '') { $where[] = 'mca.certification_type = ?'; $params[] = $certType; }
if ($level !== '') { $where[] = 'mca.account_level = ?'; $params[] = $level; }
if ($ownerType !== '') { $where[] = 'sa.owner_type = ?'; $params[] = $ownerType; }
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare(
"SELECT COUNT(*) FROM social_accounts sa
LEFT JOIN media_commercial_attributes mca ON mca.social_account_id = sa.id
WHERE $whereSql"
);
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT sa.id, sa.owner_type, sa.owner_id, sa.platform, sa.account_id, sa.profile_url,
sa.remark, sa.is_primary, sa.is_active, sa.created_at,
mca.account_level, mca.content_categories, mca.follower_count, mca.avg_read_count,
mca.certification_type, mca.special_requirements, mca.media_remark,
CASE sa.owner_type
WHEN 'person' THEN (SELECT p.full_name FROM persons p WHERE p.id = sa.owner_id)
WHEN 'company' THEN (SELECT c.display_name FROM companies c WHERE c.id = sa.owner_id)
ELSE NULL END AS owner_name
FROM social_accounts sa
LEFT JOIN media_commercial_attributes mca ON mca.social_account_id = sa.id
WHERE $whereSql
ORDER BY sa.id DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
// 供前端筛选下拉使用:平台/认证类型/等级 字典
$dict = [
'platforms' => $pdo->query("SELECT DISTINCT platform FROM social_accounts WHERE is_active = 1 ORDER BY platform")->fetchAll(PDO::FETCH_COLUMN),
'certification_types' => $pdo->query("SELECT DISTINCT certification_type FROM media_commercial_attributes WHERE certification_type IS NOT NULL AND certification_type <> '' ORDER BY certification_type")->fetchAll(PDO::FETCH_COLUMN),
'account_levels' => $pdo->query("SELECT DISTINCT account_level FROM media_commercial_attributes ORDER BY account_level")->fetchAll(PDO::FETCH_COLUMN),
];
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit, 'dict' => $dict]);
+54
View File
@@ -0,0 +1,54 @@
<?php
/**
* 媒体数据编辑接口 POST /api/media/update.php
* 入参:id + 白名单字段(含商业属性)
*/
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('media');
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$check = $pdo->prepare("SELECT * FROM social_accounts WHERE id = ?");
$check->execute([$id]);
$old = $check->fetch();
if (!$old) {
Response::error('媒体账号不存在');
}
$data = extractFields(MEDIA_FIELDS);
unset($data['owner_type'], $data['owner_id']); // 归属关系不允许改
if (!empty($data)) {
[$sets, $params] = buildUpdate($data);
$params[] = $id;
$pdo->prepare("UPDATE social_accounts SET $sets WHERE id = ?")->execute($params);
}
// 更新商业属性(存在则更新,不存在则插入)
$attr = extractFields(MEDIA_ATTR_FIELDS);
if (!empty($attr)) {
$exists = $pdo->prepare("SELECT COUNT(*) FROM media_commercial_attributes WHERE social_account_id = ?");
$exists->execute([$id]);
if ((int)$exists->fetchColumn() > 0) {
[$attrSets, $attrParams] = buildUpdate($attr);
$attrParams[] = $id;
$pdo->prepare("UPDATE media_commercial_attributes SET $attrSets WHERE social_account_id = ?")->execute($attrParams);
} else {
$attr['social_account_id'] = $id;
[$attrSql, $attrParams] = buildInsert($attr);
$pdo->prepare("INSERT INTO media_commercial_attributes $attrSql")->execute($attrParams);
}
}
logCurrent('update', 'media', 'social_accounts', $id, ['before' => $old, 'after' => $data, 'attr' => $attr]);
Response::success(null, '更新成功');
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* 需求新增接口 POST /api/need/add.php
* 必填:company_id;可选白名单字段见 NEED_FIELDS
*/
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('need');
$data = extractFields(NEED_FIELDS);
if (empty($data['company_id'])) {
Response::error('所属公司(company_id)为必填项', 400);
}
$pdo = DB::getInstance()->getPdo();
$chk = $pdo->prepare("SELECT COUNT(*) FROM companies WHERE id = ?");
$chk->execute([$data['company_id']]);
if ((int)$chk->fetchColumn() === 0) {
Response::error('所属公司不存在', 400);
}
[$sql, $params] = buildInsert($data);
$pdo->prepare("INSERT INTO company_needs $sql")->execute($params);
$newId = (int)$pdo->lastInsertId();
logCurrent('add', 'need', 'company_needs', $newId, $data);
Response::success(['id' => $newId], '新增成功');
+36
View File
@@ -0,0 +1,36 @@
<?php
/**
* 需求删除接口(作废) POST /api/need/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('need');
$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("UPDATE company_needs SET is_valid = 0 WHERE id IN ($in)");
$stmt->execute($idList);
$affected = $stmt->rowCount();
logCurrent('delete', 'need', 'company_needs', null, ['ids' => $idList]);
Response::success(['affected' => $affected], "已作废 $affected 条需求");
+29
View File
@@ -0,0 +1,29 @@
<?php
/**
* 需求详情接口 GET /api/need/detail.php?id=1
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('need');
$id = (int)($_REQUEST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare(
"SELECT cn.*, c.display_name, c.industry, c.country, c.website
FROM company_needs cn
LEFT JOIN companies c ON c.id = cn.company_id
WHERE cn.id = ?"
);
$stmt->execute([$id]);
$need = $stmt->fetch();
if (!$need) {
Response::error('需求不存在');
}
Response::success(['need' => $need]);
+61
View File
@@ -0,0 +1,61 @@
<?php
/**
* 需求转盘列表接口 GET/POST /api/need/list.php
* 参数:page / limit / keyword / start_date / end_date / company_name / industry / need_category / is_valid
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('need');
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
$startDate = trim($_REQUEST['start_date'] ?? '');
$endDate = trim($_REQUEST['end_date'] ?? '');
$companyName = trim($_REQUEST['company_name'] ?? '');
$industry = trim($_REQUEST['industry'] ?? '');
$category = trim($_REQUEST['need_category'] ?? '');
$valid = isset($_REQUEST['is_valid']) && $_REQUEST['is_valid'] !== '' ? (int)$_REQUEST['is_valid'] : null;
$where = ['cn.is_valid = 1'];
$params = [];
if ($valid !== null) {
$where = ['cn.is_valid = ?'];
$params = [$valid];
}
if ($keyword !== '') {
$where[] = '(cn.description LIKE ? OR cn.contact_person LIKE ? OR c.display_name LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like, $like);
}
if ($startDate !== '') { $where[] = 'DATE(cn.created_at) >= ?'; $params[] = $startDate; }
if ($endDate !== '') { $where[] = 'DATE(cn.created_at) <= ?'; $params[] = $endDate; }
if ($companyName !== '') { $where[] = 'c.display_name LIKE ?'; $params[] = "%$companyName%"; }
if ($industry !== '') { $where[] = 'c.industry = ?'; $params[] = $industry; }
if ($category !== '') { $where[] = 'cn.need_category = ?'; $params[] = $category; }
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare(
"SELECT COUNT(*) FROM company_needs cn LEFT JOIN companies c ON c.id = cn.company_id WHERE $whereSql"
);
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT cn.id, cn.company_id, c.display_name, c.industry, cn.contact_person,
cn.need_category, cn.target_product_category, cn.application_scenario,
cn.description, cn.is_valid, cn.created_at
FROM company_needs cn
LEFT JOIN companies c ON c.id = cn.company_id
WHERE $whereSql
ORDER BY cn.id DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
+46
View File
@@ -0,0 +1,46 @@
<?php
/**
* 需求推送接口(占位) POST /api/need/push.php
* 入参:id(或 ids 逗号分隔)
* 说明:当前为占位实现,仅写日志并返回成功;
* 后续可扩展为调用第三方API推送JSON数据 / 通过SMTP发送邮件。
*/
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('need');
$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("SELECT id, company_id FROM company_needs WHERE id IN ($in) AND is_valid = 1");
$stmt->execute($idList);
$needs = $stmt->fetchAll();
if (!$needs) {
Response::error('没有可推送的有效需求');
}
// TODO: 在此接入第三方API推送 / SMTP邮件发送
// 示例:
// foreach ($needs as $n) { sendToThirdParty($n); sendEmail($n); }
logCurrent('push', 'need', 'company_needs', null, ['ids' => $idList, 'count' => count($needs)]);
Response::success(['count' => count($needs)], "已推送 " . count($needs) . " 条需求(占位实现,后续接入第三方API/邮件)");
+42
View File
@@ -0,0 +1,42 @@
<?php
/**
* 需求编辑接口 POST /api/need/update.php
* 入参:id + 白名单字段(可含 is_valid 作废/恢复)
*/
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('need');
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
// is_valid 允许单独传
$data = extractFields(NEED_FIELDS);
if (isset($_POST['is_valid']) && $_POST['is_valid'] !== '') {
$data['is_valid'] = (int)$_POST['is_valid'] ? 1 : 0;
}
if (empty($data)) {
Response::error('没有需要更新的字段', 400);
}
$pdo = DB::getInstance()->getPdo();
$check = $pdo->prepare("SELECT * FROM company_needs WHERE id = ?");
$check->execute([$id]);
$old = $check->fetch();
if (!$old) {
Response::error('需求不存在');
}
[$sets, $params] = buildUpdate($data);
$params[] = $id;
$pdo->prepare("UPDATE company_needs SET $sets WHERE id = ?")->execute($params);
logCurrent('update', 'need', 'company_needs', $id, ['before' => $old, 'after' => $data]);
Response::success(null, '更新成功');
+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* 人员新增接口 POST /api/person/add.php
* 必填:full_name;union_id 为空时自动生成。
* 可选:contacts(JSON 数组,如 [{"platform":"email","account_id":"a@b.com","is_primary":1}])
*/
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('person');
$data = extractFields(PERSON_FIELDS);
if (empty($data['full_name'])) {
Response::error('姓名(full_name)为必填项', 400);
}
if (empty($data['union_id'])) {
$data['union_id'] = 'P' . date('YmdHis') . substr(uniqid(), -6);
}
$pdo = DB::getInstance()->getPdo();
// union_id 唯一性
$chk = $pdo->prepare("SELECT COUNT(*) FROM persons WHERE union_id = ?");
$chk->execute([$data['union_id']]);
if ((int)$chk->fetchColumn() > 0) {
Response::error('union_id 已存在,请更换');
}
[$sql, $params] = buildInsert($data);
$pdo->prepare("INSERT INTO persons $sql")->execute($params);
$newId = (int)$pdo->lastInsertId();
// 写入联系方式
$contacts = json_decode($_POST['contacts'] ?? '[]', true);
if (is_array($contacts)) {
$ins = $pdo->prepare(
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary)
VALUES ('person', ?, ?, ?, ?, ?, ?)"
);
foreach ($contacts as $c) {
$platform = trim($c['platform'] ?? '');
$accountId = trim($c['account_id'] ?? '');
if ($platform === '' || $accountId === '') continue;
$ins->execute([$newId, $platform, $accountId, $c['profile_url'] ?? null, $c['remark'] ?? null, !empty($c['is_primary']) ? 1 : 0]);
}
}
logCurrent('add', 'person', 'persons', $newId, ['data' => $data, 'contacts' => $contacts]);
Response::success(['id' => $newId], '新增成功');
+52
View File
@@ -0,0 +1,52 @@
<?php
/**
* 人员任职公司列表接口 GET /api/person/companies.php
* 入参:person_id(必填)/ page / limit
* 返回:该人员任职的公司列表(公司名称/部门/岗位/职级/入职日期/是否在岗)
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('person');
$personId = (int)($_REQUEST['person_id'] ?? 0);
if ($personId <= 0) {
Response::error('参数错误', 400);
}
[$page, $limit] = pageParams(5);
$pdo = DB::getInstance()->getPdo();
$chk = $pdo->prepare("SELECT COUNT(*) FROM persons WHERE id = ?");
$chk->execute([$personId]);
if ((int)$chk->fetchColumn() === 0) {
Response::error('人员不存在');
}
$stmt = $pdo->prepare(
"SELECT COUNT(*) FROM person_work_experiences pwe
INNER JOIN companies c ON c.id = pwe.company_id
WHERE pwe.person_id = ? AND pwe.is_active = 1"
);
$stmt->execute([$personId]);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT pwe.id AS work_id, pwe.company_id,
CASE WHEN c.name_zh IS NOT NULL AND c.name_zh <> '' THEN c.name_zh
ELSE COALESCE(c.name_en, c.display_name) END AS company_name,
c.industry, pwe.department, pwe.position, pwe.job_level,
pwe.start_date, pwe.end_date, pwe.is_current
FROM person_work_experiences pwe
INNER JOIN companies c ON c.id = pwe.company_id
WHERE pwe.person_id = ? AND pwe.is_active = 1
ORDER BY pwe.is_current DESC, pwe.start_date DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute([$personId]);
$list = $stmt->fetchAll();
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
+36
View File
@@ -0,0 +1,36 @@
<?php
/**
* 人员删除接口(软删除) POST /api/person/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('person');
$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("UPDATE persons SET is_active = 0 WHERE id IN ($in)");
$stmt->execute($idList);
$affected = $stmt->rowCount();
logCurrent('delete', 'person', 'persons', null, ['ids' => $idList]);
Response::success(['affected' => $affected], "已删除 $affected 条记录");
+38
View File
@@ -0,0 +1,38 @@
<?php
/**
* 人员详情接口 GET /api/person/detail.php?id=1
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('person');
$id = (int)($_REQUEST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT * FROM persons WHERE id = ?");
$stmt->execute([$id]);
$person = $stmt->fetch();
if (!$person) {
Response::error('人员不存在');
}
$accounts = $pdo->prepare("SELECT id, platform, account_id, profile_url, remark, is_primary, is_active FROM social_accounts WHERE owner_type = 'person' AND owner_id = ? ORDER BY is_primary DESC, id DESC");
$accounts->execute([$id]);
$experiences = $pdo->prepare(
"SELECT pwe.id, pwe.company_id, c.display_name, c.industry, pwe.position, pwe.department,
pwe.job_level, pwe.start_date, pwe.end_date, pwe.is_current
FROM person_work_experiences pwe
LEFT JOIN companies c ON c.id = pwe.company_id
WHERE pwe.person_id = ? AND pwe.is_active = 1
ORDER BY pwe.is_current DESC, pwe.start_date DESC"
);
$experiences->execute([$id]);
Response::success(['person' => $person, 'accounts' => $accounts->fetchAll(), 'experiences' => $experiences->fetchAll()]);
+59
View File
@@ -0,0 +1,59 @@
<?php
/**
* 人员导出接口(CSV) GET/POST /api/person/export.php
* 参数同 list.php
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
require_once __DIR__ . '/../common/logger.php';
checkPermission('person');
// 仅导出勾选的人员
$idsRaw = trim($_REQUEST['ids'] ?? '');
$idList = [];
if ($idsRaw !== '') {
foreach (explode(',', $idsRaw) as $v) {
$v = (int)trim($v);
if ($v > 0) {
$idList[] = $v;
}
}
}
$idList = array_unique($idList);
if (!$idList) {
Response::error('请先选择需要导出的人员数据', 1);
}
$where = ['p.id IN (' . implode(',', array_fill(0, count($idList), '?')) . ')', 'p.is_active = 1'];
$params = $idList;
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare(
"SELECT p.id, p.union_id, p.full_name, p.gender, p.nationality, p.id_type, p.id_number,
p.education, p.graduated_from, p.hometown, p.work_location, p.created_at,
(SELECT sa.account_id FROM social_accounts sa WHERE sa.owner_type='person' AND sa.owner_id=p.id AND sa.platform='phone' AND sa.is_active=1 LIMIT 1) AS phone,
(SELECT sa.account_id FROM social_accounts sa WHERE sa.owner_type='person' AND sa.owner_id=p.id AND sa.platform='email' AND sa.is_active=1 LIMIT 1) AS email
FROM persons p WHERE $whereSql ORDER BY p.id DESC"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
logCurrent('export', 'person', 'persons', null, ['count' => count($list)]);
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="persons_' . date('Ymd_His') . '.csv"');
echo "\xEF\xBB\xBF";
$out = fopen('php://output', 'w');
fputcsv($out, ['ID', 'union_id', '姓名', '性别', '国籍', '证件类型', '证件号', '学历', '毕业院校', '家乡', '工作所在地', '手机', '邮箱', '创建时间']);
foreach ($list as $r) {
fputcsv($out, [
$r['id'], $r['union_id'], $r['full_name'], $r['gender'], $r['nationality'],
$r['id_type'], $r['id_number'], $r['education'], $r['graduated_from'],
$r['hometown'], $r['work_location'], $r['phone'], $r['email'], $r['created_at'],
]);
}
fclose($out);
exit;
+82
View File
@@ -0,0 +1,82 @@
<?php
/**
* 人员 CSV 导入接口 POST /api/person/import.php (multipart/form-data, 字段名 file)
* CSV 表头:union_id,full_name,gender,nationality,id_type,id_number,education,
* graduated_from,hometown,work_location,phone,email
* 仅 full_name 必填;union_id 留空自动生成;phone/email 写入 social_accounts。
*/
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('person');
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
Response::error('请选择要上传的CSV文件', 400);
}
$handle = fopen($_FILES['file']['tmp_name'], 'r');
if (!$handle) {
Response::error('无法读取文件', 400);
}
$first = preg_replace('/^\xEF\xBB\xBF/', '', fgets($handle));
$header = str_getcsv(trim($first));
$pdo = DB::getInstance()->getPdo();
$insPerson = $pdo->prepare(
"INSERT INTO persons (union_id, full_name, gender, nationality, id_type, id_number, education, graduated_from, hometown, work_location)
VALUES (?,?,?,?,?,?,?,?,?,?)"
);
$insAccount = $pdo->prepare(
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, is_primary)
VALUES ('person', ?, ?, ?, ?)"
);
$inserted = 0;
$failed = 0;
while (($row = fgetcsv($handle)) !== false) {
$row = array_map('trim', $row);
$rec = [];
foreach ($header as $idx => $col) {
$col = trim($col);
if (isset($row[$idx])) {
$rec[$col] = $row[$idx];
}
}
if (empty($rec['full_name'])) {
$failed++;
continue;
}
try {
$unionId = !empty($rec['union_id']) ? $rec['union_id'] : ('P' . date('YmdHis') . substr(uniqid(), -6));
// 跳过重复 union_id
$chk = $pdo->prepare("SELECT COUNT(*) FROM persons WHERE union_id = ?");
$chk->execute([$unionId]);
if ((int)$chk->fetchColumn() > 0) {
$failed++;
continue;
}
$insPerson->execute([
$unionId, $rec['full_name'], $rec['gender'] ?? '保密', $rec['nationality'] ?? null,
$rec['id_type'] ?? null, $rec['id_number'] ?? null, $rec['education'] ?? null,
$rec['graduated_from'] ?? null, $rec['hometown'] ?? null, $rec['work_location'] ?? null,
]);
$newId = (int)$pdo->lastInsertId();
if (!empty($rec['phone'])) {
$insAccount->execute([$newId, 'phone', $rec['phone'], 1]);
}
if (!empty($rec['email'])) {
$insAccount->execute([$newId, 'email', $rec['email'], 0]);
}
$inserted++;
} catch (Exception $e) {
$failed++;
}
}
fclose($handle);
logCurrent('import', 'person', 'persons', null, ['inserted' => $inserted, 'failed' => $failed]);
Response::success(['inserted' => $inserted, 'failed' => $failed], "导入完成:成功 $inserted 条,失败 $failed 条");
+61
View File
@@ -0,0 +1,61 @@
<?php
/**
* 人员列表接口 GET/POST /api/person/list.php
* 参数:page / limit / keyword(姓名/证件号/手机/邮箱)/ gender / nationality / work_location / is_active
* 返回:{ list, total, page, limit }
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('person');
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
$gender = trim($_REQUEST['gender'] ?? '');
$nationality = trim($_REQUEST['nationality'] ?? '');
$location = trim($_REQUEST['work_location'] ?? '');
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
$where = ['p.is_active = 1'];
$params = [];
if ($active !== null) {
$where = ['p.is_active = ?'];
$params = [$active];
}
if ($keyword !== '') {
// 姓名/证件号直查;手机/邮箱通过 social_accounts 关联
$where[] = "(p.full_name LIKE ? OR p.id_number LIKE ? OR p.union_id LIKE ? OR EXISTS (
SELECT 1 FROM social_accounts sa
WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.is_active = 1
AND sa.account_id LIKE ?
))";
$like = "%$keyword%";
array_push($params, $like, $like, $like, $like);
}
if ($gender !== '') { $where[] = 'p.gender = ?'; $params[] = $gender; }
if ($nationality !== '') { $where[] = 'p.nationality = ?'; $params[] = $nationality; }
if ($location !== '') { $where[] = 'p.work_location LIKE ?'; $params[] = "%$location%"; }
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM persons p WHERE $whereSql");
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT p.id, p.union_id, p.full_name, p.gender, p.nationality, p.id_type, p.id_number,
p.education, p.graduated_from, p.hometown, p.work_location, p.is_active, p.created_at,
(SELECT GROUP_CONCAT(CONCAT(sa.platform, ':', sa.account_id) SEPARATOR ' | ')
FROM social_accounts sa WHERE sa.owner_type = 'person' AND sa.owner_id = p.id AND sa.is_active = 1) AS contacts
FROM persons p
WHERE $whereSql
ORDER BY p.id DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
+58
View File
@@ -0,0 +1,58 @@
<?php
/**
* 人员编辑接口 POST /api/person/update.php
* 入参:id + 白名单字段;可选 contacts(JSON 数组,整体替换联系方式)
*/
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('person');
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$data = extractFields(PERSON_FIELDS);
unset($data['union_id']); // 不允许修改 union_id
$pdo = DB::getInstance()->getPdo();
$check = $pdo->prepare("SELECT * FROM persons WHERE id = ?");
$check->execute([$id]);
$old = $check->fetch();
if (!$old) {
Response::error('人员不存在');
}
$contactsChanged = false;
if (isset($_POST['contacts'])) {
$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)) {
$ins = $pdo->prepare(
"INSERT INTO social_accounts (owner_type, owner_id, platform, account_id, profile_url, remark, is_primary)
VALUES ('person', ?, ?, ?, ?, ?, ?)"
);
foreach ($contacts as $c) {
$platform = trim($c['platform'] ?? '');
$accountId = trim($c['account_id'] ?? '');
if ($platform === '' || $accountId === '') continue;
$ins->execute([$id, $platform, $accountId, $c['profile_url'] ?? null, $c['remark'] ?? null, !empty($c['is_primary']) ? 1 : 0]);
}
}
}
if (!empty($data)) {
[$sets, $params] = buildUpdate($data);
$params[] = $id;
$pdo->prepare("UPDATE persons SET $sets WHERE id = ?")->execute($params);
}
logCurrent('update', 'person', 'persons', $id, ['before' => $old, 'after' => $data, 'contacts_replaced' => $contactsChanged]);
Response::success(null, '更新成功');
+51
View File
@@ -0,0 +1,51 @@
<?php
/**
* 碎片处理列表接口(待定逻辑,暂做基础列表) GET/POST /api/preliminary/list.php
* 参数:page / limit / keyword / status / source_type / start_date / end_date
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('preliminary');
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
$status = trim($_REQUEST['status'] ?? '');
$sourceType = trim($_REQUEST['source_type'] ?? '');
$startDate = trim($_REQUEST['start_date'] ?? '');
$endDate = trim($_REQUEST['end_date'] ?? '');
$where = ['is_active = 1'];
$params = [];
if ($keyword !== '') {
$where[] = '(company_name LIKE ? OR person_name LIKE ? OR contact_phone LIKE ? OR contact_email LIKE ? OR description LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like, $like, $like, $like);
}
if ($status !== '') { $where[] = 'status = ?'; $params[] = $status; }
if ($sourceType !== '') { $where[] = 'source_type = ?'; $params[] = $sourceType; }
if ($startDate !== '') { $where[] = 'DATE(created_at) >= ?'; $params[] = $startDate; }
if ($endDate !== '') { $where[] = 'DATE(created_at) <= ?'; $params[] = $endDate; }
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM preliminary_data WHERE $whereSql");
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, source_type, company_name, person_name, contact_phone, contact_email,
target_product_category, description, status, source_channel, recorded_by,
follow_person, converted_type, converted_id, created_at, updated_at
FROM preliminary_data
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]);
+30
View File
@@ -0,0 +1,30 @@
<?php
/**
* 碎片处理统计接口 GET /api/preliminary/stats.php
* 返回:总数 / 今日新增 / 各状态数量
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('preliminary');
$pdo = DB::getInstance()->getPdo();
$total = (int)$pdo->query("SELECT COUNT(*) FROM preliminary_data WHERE is_active = 1")->fetchColumn();
$today = (int)$pdo->query("SELECT COUNT(*) FROM preliminary_data WHERE is_active = 1 AND DATE(created_at) = CURDATE()")->fetchColumn();
$statusRows = $pdo->query(
"SELECT status, COUNT(*) AS cnt FROM preliminary_data WHERE is_active = 1 GROUP BY status"
)->fetchAll();
$statusMap = [];
foreach ($statusRows as $r) {
$statusMap[$r['status']] = (int)$r['cnt'];
}
Response::success([
'total' => $total,
'today' => $today,
'status' => $statusMap,
'statuses' => ['待处理', '待分配', '处理中', '已转换', '已废弃', '已拒收'],
]);
+58
View File
@@ -0,0 +1,58 @@
<?php
/**
* 操作日志查询接口 GET/POST /api/system/log_list.php
* 仅超级管理员可见,默认查询近1个月
* 参数:page / limit / keyword / action / module / username / start_date / end_date
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
requireSuperAdmin();
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
$action = trim($_REQUEST['action'] ?? '');
$module = trim($_REQUEST['module'] ?? '');
$username = trim($_REQUEST['username'] ?? '');
$startDate = trim($_REQUEST['start_date'] ?? '');
$endDate = trim($_REQUEST['end_date'] ?? '');
$where = ['created_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)'];
$params = [];
if ($keyword !== '') {
$where[] = '(username LIKE ? OR content LIKE ? OR module LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like, $like);
}
if ($action !== '') { $where[] = 'action = ?'; $params[] = $action; }
if ($module !== '') { $where[] = 'module = ?'; $params[] = $module; }
if ($username !== '') { $where[] = 'username = ?'; $params[] = $username; }
if ($startDate !== '') { $where[] = 'DATE(created_at) >= ?'; $params[] = $startDate; }
if ($endDate !== '') { $where[] = 'DATE(created_at) <= ?'; $params[] = $endDate; }
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM system_logs WHERE $whereSql");
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, user_id, username, action, module, target_table, target_id, content, ip, created_at
FROM system_logs
WHERE $whereSql
ORDER BY id DESC
LIMIT $limit OFFSET $offset"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
// 筛选下拉字典
$dict = [
'actions' => $pdo->query("SELECT DISTINCT action FROM system_logs ORDER BY action")->fetchAll(PDO::FETCH_COLUMN),
'modules' => $pdo->query("SELECT DISTINCT module FROM system_logs ORDER BY module")->fetchAll(PDO::FETCH_COLUMN),
];
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit, 'dict' => $dict]);
+37
View File
@@ -0,0 +1,37 @@
<?php
/**
* 新增角色接口(含权限分配) POST /api/system/role_add.php
* 入参:role_name / permissions(JSON数组,菜单标识)/ is_active
*/
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('system');
$roleName = trim($_POST['role_name'] ?? '');
$perms = json_decode($_POST['permissions'] ?? '[]', true);
$isActive = isset($_POST['is_active']) ? ((int)$_POST['is_active'] ? 1 : 0) : 1;
if ($roleName === '') {
Response::error('角色名称不能为空', 400);
}
if (!is_array($perms)) {
Response::error('权限格式错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$chk = $pdo->prepare("SELECT COUNT(*) FROM system_roles WHERE role_name = ?");
$chk->execute([$roleName]);
if ((int)$chk->fetchColumn() > 0) {
Response::error('角色名称已存在');
}
$stmt = $pdo->prepare("INSERT INTO system_roles (role_name, permissions, is_active) VALUES (?, ?, ?)");
$stmt->execute([$roleName, json_encode(array_values($perms), JSON_UNESCAPED_UNICODE), $isActive]);
$newId = (int)$pdo->lastInsertId();
logCurrent('add', 'system', 'system_roles', $newId, ['role_name' => $roleName, 'permissions' => $perms]);
Response::success(['id' => $newId], '新增成功');
+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* 删除角色接口 POST /api/system/role_delete.php
* 入参:id(或 ids 逗号分隔批量)
* 保护:不允许删除内置超级管理员角色(id=1)及仍被用户使用的角色
*/
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('system');
$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);
}
if (in_array(1, $idList, true)) {
Response::error('不允许删除内置超级管理员角色', 400);
}
$pdo = DB::getInstance()->getPdo();
// 检查角色是否被使用
$in = implode(',', array_fill(0, count($idList), '?'));
$stmt = $pdo->prepare("SELECT role_id, COUNT(*) AS cnt FROM system_users WHERE role_id IN ($in) GROUP BY role_id");
$stmt->execute($idList);
$inUse = $stmt->fetchAll();
if ($inUse) {
$names = [];
foreach ($inUse as $r) {
$names[] = "角色ID {$r['role_id']}({$r['cnt']}个用户)";
}
Response::error('以下角色仍被用户使用,无法删除:' . implode('、', $names), 400);
}
$del = $pdo->prepare("DELETE FROM system_roles WHERE id IN ($in)");
$del->execute($idList);
$affected = $del->rowCount();
logCurrent('delete', 'system', 'system_roles', null, ['ids' => $idList]);
Response::success(['affected' => $affected], "已删除 $affected 个角色");
+47
View File
@@ -0,0 +1,47 @@
<?php
/**
* 角色列表接口 GET/POST /api/system/role_list.php
* 参数:page / limit / keyword / is_active
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('system');
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
$where = ['1=1'];
$params = [];
if ($keyword !== '') {
$where[] = 'role_name LIKE ?';
$params[] = "%$keyword%";
}
if ($active !== null) { $where[] = 'is_active = ?'; $params[] = $active; }
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM system_roles WHERE $whereSql");
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT id, role_name, permissions, is_active, created_at,
(SELECT COUNT(*) FROM system_users u WHERE u.role_id = system_roles.id) AS user_count
FROM system_roles
WHERE $whereSql
ORDER BY id ASC
LIMIT $limit OFFSET $offset"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
foreach ($list as &$r) {
$r['permissions'] = json_decode($r['permissions'], true) ?: [];
}
unset($r);
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]);
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* 编辑角色接口(含权限分配) POST /api/system/role_update.php
* 入参:id / role_name / permissions(JSON数组)/ is_active
*/
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('system');
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$check = $pdo->prepare("SELECT * FROM system_roles WHERE id = ?");
$check->execute([$id]);
$old = $check->fetch();
if (!$old) {
Response::error('角色不存在');
}
// 超级管理员角色保护:不允许移除其自身
if ($id === 1) {
Response::error('内置超级管理员角色不允许修改', 400);
}
$sets = [];
$params = [];
if (isset($_POST['role_name'])) {
$roleName = trim($_POST['role_name']);
if ($roleName === '') {
Response::error('角色名称不能为空', 400);
}
$chk = $pdo->prepare("SELECT COUNT(*) FROM system_roles WHERE role_name = ? AND id <> ?");
$chk->execute([$roleName, $id]);
if ((int)$chk->fetchColumn() > 0) {
Response::error('角色名称已存在');
}
$sets[] = 'role_name = ?';
$params[] = $roleName;
}
if (isset($_POST['permissions'])) {
$perms = json_decode($_POST['permissions'], true);
if (!is_array($perms)) {
Response::error('权限格式错误', 400);
}
$sets[] = 'permissions = ?';
$params[] = json_encode(array_values($perms), JSON_UNESCAPED_UNICODE);
}
if (isset($_POST['is_active'])) {
$sets[] = 'is_active = ?';
$params[] = (int)$_POST['is_active'] ? 1 : 0;
}
if (!$sets) {
Response::error('没有需要更新的字段', 400);
}
$params[] = $id;
$pdo->prepare("UPDATE system_roles SET " . implode(', ', $sets) . " WHERE id = ?")->execute($params);
logCurrent('update', 'system', 'system_roles', $id, ['before' => $old, 'after' => $sets]);
Response::success(null, '更新成功');
+52
View File
@@ -0,0 +1,52 @@
<?php
/**
* 新增用户接口 POST /api/system/user_add.php
* 入参:username / password / real_name / role_id / is_active
*/
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('system');
$username = trim($_POST['username'] ?? '');
$password = (string)($_POST['password'] ?? '');
$realName = trim($_POST['real_name'] ?? '');
$roleId = (int)($_POST['role_id'] ?? 0);
$isActive = isset($_POST['is_active']) ? ((int)$_POST['is_active'] ? 1 : 0) : 1;
if ($username === '' || !preg_match('/^[a-zA-Z0-9_]{3,50}$/', $username)) {
Response::error('账号需为3-50位字母/数字/下划线', 400);
}
if (strlen($password) < 6) {
Response::error('密码长度不能少于6位', 400);
}
if ($roleId <= 0) {
Response::error('请选择角色', 400);
}
$pdo = DB::getInstance()->getPdo();
$chk = $pdo->prepare("SELECT COUNT(*) FROM system_users WHERE username = ?");
$chk->execute([$username]);
if ((int)$chk->fetchColumn() > 0) {
Response::error('账号已存在');
}
$chkRole = $pdo->prepare("SELECT COUNT(*) FROM system_roles WHERE id = ?");
$chkRole->execute([$roleId]);
if ((int)$chkRole->fetchColumn() === 0) {
Response::error('角色不存在', 400);
}
$stmt = $pdo->prepare(
"INSERT INTO system_users (username, password, real_name, role_id, is_active)
VALUES (?, ?, ?, ?, ?)"
);
$stmt->execute([$username, sha1($password), $realName !== '' ? $realName : null, $roleId, $isActive]);
$newId = (int)$pdo->lastInsertId();
logCurrent('add', 'system', 'system_users', $newId, ['username' => $username, 'role_id' => $roleId, 'is_active' => $isActive]);
Response::success(['id' => $newId], '新增成功');
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* 删除用户接口 POST /api/system/user_delete.php
* 入参:id(或 ids 逗号分隔批量)
* 保护:不允许删除内置管理员(id=1)
*/
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('system');
$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);
}
if (in_array(1, $idList, true)) {
Response::error('不允许删除内置管理员账号', 400);
}
$pdo = DB::getInstance()->getPdo();
$in = implode(',', array_fill(0, count($idList), '?'));
$stmt = $pdo->prepare("DELETE FROM system_users WHERE id IN ($in)");
$stmt->execute($idList);
$affected = $stmt->rowCount();
logCurrent('delete', 'system', 'system_users', null, ['ids' => $idList]);
Response::success(['affected' => $affected], "已删除 $affected 个用户");
+50
View File
@@ -0,0 +1,50 @@
<?php
/**
* 用户列表接口 GET/POST /api/system/user_list.php
* 参数:page / limit / keyword / role_id / is_active
*/
require_once __DIR__ . '/../common/db.php';
require_once __DIR__ . '/../common/response.php';
require_once __DIR__ . '/../common/auth.php';
checkPermission('system');
[$page, $limit] = pageParams();
$keyword = trim($_REQUEST['keyword'] ?? '');
$roleId = (int)($_REQUEST['role_id'] ?? 0);
$active = isset($_REQUEST['is_active']) && $_REQUEST['is_active'] !== '' ? (int)$_REQUEST['is_active'] : null;
$where = ['1=1'];
$params = [];
if ($keyword !== '') {
$where[] = '(u.username LIKE ? OR u.real_name LIKE ?)';
$like = "%$keyword%";
array_push($params, $like, $like);
}
if ($roleId > 0) { $where[] = 'u.role_id = ?'; $params[] = $roleId; }
if ($active !== null) { $where[] = 'u.is_active = ?'; $params[] = $active; }
$whereSql = implode(' AND ', $where);
$pdo = DB::getInstance()->getPdo();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM system_users u WHERE $whereSql");
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare(
"SELECT u.id, u.username, u.real_name, u.role_id, r.role_name, u.is_active,
u.last_login_time, u.last_login_ip, u.created_at
FROM system_users u
LEFT JOIN system_roles r ON r.id = u.role_id
WHERE $whereSql
ORDER BY u.id ASC
LIMIT $limit OFFSET $offset"
);
$stmt->execute($params);
$list = $stmt->fetchAll();
// 角色下拉
$roles = $pdo->query("SELECT id, role_name FROM system_roles WHERE is_active = 1 ORDER BY id")->fetchAll();
Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit, 'roles' => $roles]);
+70
View File
@@ -0,0 +1,70 @@
<?php
/**
* 编辑用户接口(含角色分配) POST /api/system/user_update.php
* 入参:id / real_name / role_id / is_active / password(可选,留空不改密码)
*/
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('system');
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
Response::error('参数错误', 400);
}
$pdo = DB::getInstance()->getPdo();
$check = $pdo->prepare("SELECT * FROM system_users WHERE id = ?");
$check->execute([$id]);
$old = $check->fetch();
if (!$old) {
Response::error('用户不存在');
}
// 内置管理员保护:不允许禁用 id=1(admin)
if ($id === 1 && isset($_POST['is_active']) && (int)$_POST['is_active'] === 0) {
Response::error('不允许禁用内置管理员账号', 400);
}
$sets = [];
$params = [];
if (isset($_POST['real_name'])) {
$sets[] = 'real_name = ?';
$params[] = trim($_POST['real_name']) !== '' ? trim($_POST['real_name']) : null;
}
if (isset($_POST['role_id'])) {
$roleId = (int)$_POST['role_id'];
$chkRole = $pdo->prepare("SELECT COUNT(*) FROM system_roles WHERE id = ?");
$chkRole->execute([$roleId]);
if ((int)$chkRole->fetchColumn() === 0) {
Response::error('角色不存在', 400);
}
$sets[] = 'role_id = ?';
$params[] = $roleId;
}
if (isset($_POST['is_active'])) {
$sets[] = 'is_active = ?';
$params[] = (int)$_POST['is_active'] ? 1 : 0;
}
$password = (string)($_POST['password'] ?? '');
if ($password !== '') {
if (strlen($password) < 6) {
Response::error('密码长度不能少于6位', 400);
}
$sets[] = 'password = ?';
$params[] = sha1($password);
}
if (!$sets) {
Response::error('没有需要更新的字段', 400);
}
$params[] = $id;
$pdo->prepare("UPDATE system_users SET " . implode(', ', $sets) . " WHERE id = ?")->execute($params);
logCurrent('update', 'system', 'system_users', $id, ['before' => ['username' => $old['username'], 'role_id' => $old['role_id'], 'is_active' => $old['is_active']]]);
Response::success(null, '更新成功');