文件还在测试中

This commit is contained in:
2026-08-08 15:53:53 +08:00
parent 5f1e7adb64
commit 8101177cb5
241 changed files with 18074 additions and 22 deletions
+123
View File
@@ -0,0 +1,123 @@
<?php
namespace App\Controllers\CRM;
use App\Controllers\Controller;
use App\Models\CRM\Contact;
use App\Models\CRM\Customer;
/** 客户联系人(一个客户下可多人)。super_admin / crm_admin 可增删改;crm_user 仅查看。 */
class ContactsController extends Controller
{
private function nav(): array
{
return [
['k' => 'dashboard', 'label' => '仪表盘', 'icon' => '📊', 'url' => 'CRM'],
['k' => 'customers', 'label' => '客户管理', 'icon' => '🤝', 'url' => 'CRM/customers'],
['k' => 'leads', 'label' => '商机线索', 'icon' => '💡', 'url' => 'CRM/leads'],
['k' => 'followups', 'label' => '跟进记录', 'icon' => '📞', 'url' => 'CRM/followups'],
['k' => 'contacts', 'label' => '客户联系人', 'icon' => '👥', 'url' => 'CRM/contacts'],
];
}
/** 列表:/CRM/contacts(全部)或 /CRM/contacts?customer_id=ID(某客户) */
public function index($customerId = null)
{
$customerId = $customerId ? (int)$customerId : (int)($_GET['customer_id'] ?? 0);
$customer = null;
if ($customerId) {
$customer = (new Customer())->find($customerId);
$contacts = (new Contact())->where('customer_id', $customerId);
} else {
$contacts = $this->allWithCustomer();
}
return $this->renderSubsys('crm', 'crm/contacts', [
'contacts' => $contacts,
'customer' => $customer,
], $this->nav(), 'contacts');
}
public function create()
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
$preId = (int)($_GET['customer_id'] ?? 0);
$customers = (new Customer())->all();
return $this->renderSubsys('crm', 'crm/contact_form', [
'contact' => null,
'customers' => $customers,
'preId' => $preId,
], $this->nav(), 'contacts');
}
public function store()
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
if (!csrf_check()) return $this->redirect('CRM/contacts');
$cid = (int)$this->post('customer_id');
$data = $this->collect();
(new Contact())->insert($data);
return $this->redirect($cid ? "CRM/contacts?customer_id={$cid}" : 'CRM/contacts');
}
public function edit($id)
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
$contact = (new Contact())->find($id);
if (!$contact) return $this->redirect('CRM/contacts');
$customers = (new Customer())->all();
return $this->renderSubsys('crm', 'crm/contact_form', [
'contact' => $contact,
'customers' => $customers,
'preId' => (int)($contact['customer_id'] ?? 0),
], $this->nav(), 'contacts');
}
public function update($id)
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
if (!csrf_check()) return $this->redirect('CRM/contacts');
$contact = (new Contact())->find($id);
if (!$contact) return $this->redirect('CRM/contacts');
$cid = (int)$this->post('customer_id');
(new Contact())->update($id, $this->collect());
return $this->redirect($cid ? "CRM/contacts?customer_id={$cid}" : 'CRM/contacts');
}
public function destroy($id)
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
$contact = (new Contact())->find($id);
$cid = $contact ? (int)($contact['customer_id'] ?? 0) : 0;
(new Contact())->delete($id);
return $this->redirect($cid ? "CRM/contacts?customer_id={$cid}" : 'CRM/contacts');
}
private function collect(): array
{
return [
'customer_id' => (int)$this->post('customer_id'),
'name' => trim($this->post('name')),
'title' => trim($this->post('title')),
'phone' => trim($this->post('phone')),
'email' => trim($this->post('email')),
'wechat' => trim($this->post('wechat')),
'is_primary' => $this->post('is_primary') ? 1 : 0,
'remark' => trim($this->post('remark')),
'created_at' => date('Y-m-d'),
];
}
/** 全部联系人 + 客户名称(用于「全部联系人」视图) */
private function allWithCustomer(): array
{
try {
$rows = \Core\Db::query(
"SELECT c.*, cu.name AS customer_name FROM crm_contacts c
LEFT JOIN crm_customers cu ON cu.id=c.customer_id
ORDER BY c.customer_id, c.id"
)->fetchAll();
return $rows;
} catch (\Throwable $e) {
return (new Contact())->all();
}
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
namespace App\Controllers\CRM;
use App\Controllers\Controller;
use App\Models\CRM\Customer;
/** 客户管理(CRM 系统)。super_admin / crm_admin 可增删改;crm_user 仅查看。 */
class CustomersController extends Controller
{
private function nav(): array
{
return [
['k' => 'dashboard', 'label' => '仪表盘', 'icon' => '📊', 'url' => 'CRM'],
['k' => 'customers', 'label' => '客户管理', 'icon' => '🤝', 'url' => 'CRM/customers'],
['k' => 'leads', 'label' => '商机线索', 'icon' => '💡', 'url' => 'CRM/leads'],
['k' => 'followups', 'label' => '跟进记录', 'icon' => '📞', 'url' => 'CRM/followups'],
];
}
public function index()
{
$customers = (new Customer())->all();
$contactCounts = $this->contactCounts();
return $this->renderSubsys('crm', 'crm/customers', [
'customers' => $customers,
'contactCounts' => $contactCounts,
], $this->nav(), 'customers');
}
/** 每个客户的联系人数量(键=customer_id */
private function contactCounts(): array
{
$out = [];
try {
$rows = \Core\Db::query("SELECT customer_id, COUNT(*) AS n FROM crm_contacts GROUP BY customer_id");
foreach ($rows->fetchAll() as $r) { $out[(int)$r['customer_id']] = (int)$r['n']; }
} catch (\Throwable $e) {}
return $out;
}
public function create()
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
return $this->renderSubsys('crm', 'crm/customer_form', ['customer' => null], $this->nav(), 'customers');
}
public function store()
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
if (!csrf_check()) { return $this->renderSubsys('crm', 'crm/customer_form', ['customer' => null], $this->nav(), 'customers'); }
$data = $this->collect();
(new Customer())->insert($data);
return $this->redirect('CRM/customers');
}
public function edit($id)
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
$customer = (new Customer())->find($id);
if (!$customer) return $this->redirect('CRM/customers');
return $this->renderSubsys('crm', 'crm/customer_form', ['customer' => $customer], $this->nav(), 'customers');
}
public function update($id)
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
if (!csrf_check()) return $this->redirect('CRM/customers');
$customer = (new Customer())->find($id);
if (!$customer) return $this->redirect('CRM/customers');
(new Customer())->update($id, $this->collect());
return $this->redirect('CRM/customers');
}
public function destroy($id)
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
(new Customer())->delete($id);
return $this->redirect('CRM/customers');
}
private function collect(): array
{
return [
'name' => trim($this->post('name')),
'company' => trim($this->post('company')),
'contact' => trim($this->post('contact')),
'phone' => trim($this->post('phone')),
'email' => trim($this->post('email')),
'country' => trim($this->post('country')),
'type' => $this->post('type') ?: 'brand',
'source' => trim($this->post('source')),
'level' => $this->post('level') ?: 'C',
'customer_no' => trim($this->post('customer_no')),
'industry' => trim($this->post('industry')),
'region' => trim($this->post('region')),
'credit_limit'=> (float)$this->post('credit_limit'),
'status' => $this->post('status') ?: 'lead',
'remark' => trim($this->post('remark')),
'owner' => trim($this->post('owner')) ?: ($_SESSION['admin_name'] ?? ''),
'created_at' => date('Y-m-d'),
];
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace App\Controllers\CRM;
use App\Controllers\Controller;
use App\Models\CRM\Customer;
use App\Models\CRM\Lead;
use App\Models\CRM\FollowUp;
/**
* CRM 入口与子路由
* 权限:super_admin 或 crm_role ∈ {admin,user}(路由层已拦截)。
*/
class DashboardController extends Controller
{
public function nav(string $seg): array
{
return [
['k' => 'dashboard', 'label' => '仪表盘', 'icon' => '📊', 'url' => 'CRM'],
['k' => 'customers', 'label' => '客户管理', 'icon' => '🤝', 'url' => 'CRM/customers'],
['k' => 'leads', 'label' => '商机线索', 'icon' => '💡', 'url' => 'CRM/leads'],
['k' => 'followups', 'label' => '跟进记录', 'icon' => '📞', 'url' => 'CRM/followups'],
['k' => 'contacts', 'label' => '客户联系人', 'icon' => '👥', 'url' => 'CRM/contacts'],
];
}
/** 统一子路由入口 */
public function dispatch(array $s)
{
$res = $s[0] ?? 'dashboard';
$action = $s[1] ?? '';
$id = $s[2] ?? null;
// 仪表盘始终可进
if ($res === 'dashboard') {
return $this->dashboard();
}
// 用户管理(仅该系统管理员):统一管理本系统用户及其页面权限
if ($res === 'users') {
if (!\subsys_admin('crm')) {
\Core\App::forbidden('需要 CRM 管理员权限');
return;
}
$uc = new \App\Controllers\CRM\UsersController();
if ($action === '' || $action === 'index') return $uc->index();
if ($action === 'create') return $uc->create();
if ($action === 'store') return $uc->store();
if ($action === 'edit') return $uc->edit($id);
if ($action === 'update') return $uc->update($id);
if ($action === 'destroy') return $uc->destroy($id);
if ($action === 'reset') return $uc->reset($id);
\Core\App::notFound('未知操作: ' . $action);
return;
}
$map = [
'customers' => 'CustomersController',
'leads' => 'LeadsController',
'followups' => 'FollowUpsController',
'contacts' => 'ContactsController',
];
if (!isset($map[$res])) {
\Core\App::notFound('未知页面: ' . $res);
return;
}
// 页面级权限:仪表盘始终可进,其余页面按分系统「页面可见权限」拦截
if (!\subsys_page_can('crm', $res)) {
\Core\App::forbidden('您没有访问该页面的权限');
return;
}
// 资源子操作路由:/CRM/{resource}[/{action}[/{id}]]
$method = $this->resolveSubsysAction($action);
if ($method === null) {
\Core\App::notFound('未知操作: ' . $action);
return;
}
$class = 'App\\Controllers\\CRM\\' . $map[$res];
$instance = new $class();
if (!method_exists($instance, $method)) {
\Core\App::notFound('操作不存在: ' . $method);
return;
}
return $instance->$method($id);
}
/** 将 URL 动作段解析为控制器方法名;不支持的动作返回 null */
private function resolveSubsysAction(string $action): ?string
{
$verbs = [
'' => 'index',
'index' => 'index',
'create' => 'create',
'store' => 'store',
'edit' => 'edit',
'update' => 'update',
'destroy' => 'destroy',
];
return $verbs[$action] ?? null;
}
public function dashboard()
{
$customers = (new Customer())->all();
$leads = (new Lead())->all();
$follows = (new FollowUp())->all();
$stageCount = [];
foreach ($leads as $l) {
$stageCount[$l['stage'] ?? 'new'] = ($stageCount[$l['stage'] ?? 'new'] ?? 0) + 1;
}
$amountTotal = array_sum(array_map(fn($l) => (float)($l['amount'] ?? 0), $leads));
$typeCount = [];
foreach ($customers as $c) {
$typeCount[$c['type'] ?? 'trade'] = ($typeCount[$c['type'] ?? 'trade'] ?? 0) + 1;
}
$recent = array_slice(array_reverse($follows), 0, 8);
$recentCustomers = array_slice(array_reverse($customers), 0, 8);
return $this->renderSubsys('crm', 'crm/dashboard', [
'customerTotal' => count($customers),
'leadTotal' => count($leads),
'followTotal' => count($follows),
'amountTotal' => $amountTotal,
'stageCount' => $stageCount,
'typeCount' => $typeCount,
'recent' => $recent,
'recentCustomers'=> $recentCustomers,
], $this->nav('dashboard'), 'dashboard');
}
}
@@ -0,0 +1,84 @@
<?php
namespace App\Controllers\CRM;
use App\Controllers\Controller;
use App\Models\CRM\FollowUp;
use App\Models\CRM\Customer;
/** 跟进记录(CRM 系统) */
class FollowUpsController extends Controller
{
private function nav(): array
{
return [
['k' => 'dashboard', 'label' => '仪表盘', 'icon' => '📊', 'url' => 'CRM'],
['k' => 'customers', 'label' => '客户管理', 'icon' => '🤝', 'url' => 'CRM/customers'],
['k' => 'leads', 'label' => '商机线索', 'icon' => '💡', 'url' => 'CRM/leads'],
['k' => 'followups', 'label' => '跟进记录', 'icon' => '📞', 'url' => 'CRM/followups'],
];
}
public function index()
{
$follows = (new FollowUp())->all();
$customers = (new Customer())->all();
$cmap = [];
foreach ($customers as $c) { $cmap[$c['id']] = $c['name']; }
return $this->renderSubsys('crm', 'crm/followups', ['follows' => $follows, 'cmap' => $cmap], $this->nav(), 'followups');
}
public function create()
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
$customers = (new Customer())->all();
return $this->renderSubsys('crm', 'crm/followup_form', ['follow' => null, 'customers' => $customers], $this->nav(), 'followups');
}
public function store()
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
if (!csrf_check()) return $this->redirect('CRM/followups');
(new FollowUp())->insert($this->collect());
return $this->redirect('CRM/followups');
}
public function edit($id)
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
$follow = (new FollowUp())->find($id);
if (!$follow) return $this->redirect('CRM/followups');
$customers = (new Customer())->all();
return $this->renderSubsys('crm', 'crm/followup_form', ['follow' => $follow, 'customers' => $customers], $this->nav(), 'followups');
}
public function update($id)
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
if (!csrf_check()) return $this->redirect('CRM/followups');
$follow = (new FollowUp())->find($id);
if (!$follow) return $this->redirect('CRM/followups');
(new FollowUp())->update($id, $this->collect());
return $this->redirect('CRM/followups');
}
public function destroy($id)
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
(new FollowUp())->delete($id);
return $this->redirect('CRM/followups');
}
private function collect(): array
{
return [
'customer_id' => (int)$this->post('customer_id'),
'lead_id' => (int)$this->post('lead_id'),
'content' => trim($this->post('content')),
'next_at' => trim($this->post('next_at')),
'way' => trim($this->post('way')),
'result' => trim($this->post('result')),
'owner' => trim($this->post('owner')) ?: ($_SESSION['admin_name'] ?? ''),
'created_at' => date('Y-m-d'),
];
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace App\Controllers\CRM;
use App\Controllers\Controller;
use App\Models\CRM\Lead;
use App\Models\CRM\Customer;
/** 商机/线索管理(CRM 系统) */
class LeadsController extends Controller
{
private function nav(): array
{
return [
['k' => 'dashboard', 'label' => '仪表盘', 'icon' => '📊', 'url' => 'CRM'],
['k' => 'customers', 'label' => '客户管理', 'icon' => '🤝', 'url' => 'CRM/customers'],
['k' => 'leads', 'label' => '商机线索', 'icon' => '💡', 'url' => 'CRM/leads'],
['k' => 'followups', 'label' => '跟进记录', 'icon' => '📞', 'url' => 'CRM/followups'],
];
}
public function index()
{
$leads = (new Lead())->all();
$customers = (new Customer())->all();
$cmap = [];
foreach ($customers as $c) { $cmap[$c['id']] = $c['name']; }
return $this->renderSubsys('crm', 'crm/leads', ['leads' => $leads, 'cmap' => $cmap], $this->nav(), 'leads');
}
public function create()
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
$customers = (new Customer())->all();
return $this->renderSubsys('crm', 'crm/lead_form', ['lead' => null, 'customers' => $customers], $this->nav(), 'leads');
}
public function store()
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
if (!csrf_check()) return $this->redirect('CRM/leads');
(new Lead())->insert($this->collect());
return $this->redirect('CRM/leads');
}
public function edit($id)
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
$lead = (new Lead())->find($id);
if (!$lead) return $this->redirect('CRM/leads');
$customers = (new Customer())->all();
return $this->renderSubsys('crm', 'crm/lead_form', ['lead' => $lead, 'customers' => $customers], $this->nav(), 'leads');
}
public function update($id)
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
if (!csrf_check()) return $this->redirect('CRM/leads');
$lead = (new Lead())->find($id);
if (!$lead) return $this->redirect('CRM/leads');
(new Lead())->update($id, $this->collect());
return $this->redirect('CRM/leads');
}
public function destroy($id)
{
subsys_admin('crm') or \Core\App::forbidden('需要 CRM 管理员权限');
(new Lead())->delete($id);
return $this->redirect('CRM/leads');
}
private function collect(): array
{
return [
'customer_id' => (int)$this->post('customer_id'),
'title' => trim($this->post('title')),
'amount' => (float)$this->post('amount'),
'stage' => $this->post('stage') ?: 'new',
'expected_close' => trim($this->post('expected_close')),
'source' => trim($this->post('source')),
'probability' => (int)$this->post('probability'),
'owner' => trim($this->post('owner')) ?: ($_SESSION['admin_name'] ?? ''),
'remark' => trim($this->post('remark')),
'created_at' => date('Y-m-d'),
];
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace App\Controllers\CRM;
use App\Controllers\Subsys\UsersController as BaseUsersController;
class UsersController extends BaseUsersController
{
protected function sys(): string
{
return 'crm';
}
}