1. 项目概述
在当今企业数字化转型浪潮中,客户关系管理(CRM)系统已成为企业运营的核心基础设施。传统单体架构的CRM系统往往面临扩展性差、维护成本高等问题,而前后端分离架构凭借其灵活性、可扩展性和开发效率优势,正成为现代CRM系统的主流选择。
本项目采用Laravel11作为后端框架,Vue3作为前端框架,构建了一套完整的前后端分离CRM系统。Laravel11作为PHP生态中最成熟的全栈框架,提供了优雅的语法、完善的ORM和认证系统;Vue3则以其响应式系统和Composition API,为前端开发带来了全新的灵活性。二者的结合既保证了后端API的稳定性和安全性,又实现了前端界面的动态交互体验。
2. 架构设计
2.1 整体架构
前后端分离架构的核心思想是将应用逻辑划分为明确的前后端边界:
code复制┌─────────────────────────────────────────────────────────────┐
│ 前端层 (Vue3 + Vite) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 组件层 │ │ 状态管理层 │ │ 路由层 │ │
│ │ (Components)│ │ (Pinia) │ │ (Vue Router)│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ HTTP/RESTful API
▼
┌─────────────────────────────────────────────────────────────┐
│ 后端层 (Laravel11) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 控制器层 │ │ 服务层 │ │ 模型层 │ │
│ │ (Controllers)│ │ (Services) │ │ (Models) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 数据访问层 │ │ 认证层 │ │ 中间件层 │ │
│ │ (Eloquent) │ │ (Sanctum) │ │ (Middleware)│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ MySQL │ │ Redis │ │ MinIO │
│ (数据存储) │ │ (缓存/会话) │ │ (文件存储) │
└───────────────┘ └───────────────┘ └───────────────┘
2.2 技术选型
| 层级 | 技术选型 | 作用说明 | 选型理由 |
|---|---|---|---|
| 前端框架 | Vue3 (Composition API) | 组件化开发、响应式数据绑定 | 相比Options API更灵活的逻辑复用 |
| 构建工具 | Vite5 | 极速热更新、按需编译 | 相比Webpack更快的开发体验 |
| UI组件库 | Element Plus | 企业级UI组件 | 丰富的组件和良好的文档支持 |
| 状态管理 | Pinia | 轻量级全局状态管理 | 比Vuex更简洁的API设计 |
| 后端框架 | Laravel11 | RESTful API开发、ORM、认证、队列 | PHP生态最成熟的全栈框架 |
| 认证方案 | Laravel Sanctum | 无状态Token认证 | 专为SPA设计的安全认证方案 |
| 数据库 | MySQL8.0 | 关系型数据存储 | 企业级应用的可靠选择 |
| 缓存 | Redis7.0 | 接口缓存、会话存储 | 高性能内存数据库 |
3. 后端实现
3.1 项目初始化
使用Composer创建Laravel11项目:
bash复制composer create-project laravel/laravel crm-backend "11.*"
cd crm-backend
关键配置(config/sanctum.php)启用SPA认证:
php复制'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),
3.2 数据模型设计
以客户模型为例,创建迁移文件:
bash复制php artisan make:migration create_customers_table
迁移文件设计:
php复制public function up(): void
{
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('客户名称');
$table->string('company')->nullable()->comment('所属公司');
$table->string('email')->unique()->comment('联系邮箱');
$table->string('phone')->nullable()->comment('联系电话');
$table->enum('status', ['active', 'inactive', 'blacklisted'])->default('active');
$table->foreignId('user_id')->constrained('users')->comment('负责人');
$table->softDeletes();
$table->timestamps();
$table->index('status');
$table->index('created_at');
});
}
3.3 控制器与服务层
遵循"瘦控制器、胖服务"原则:
CustomerController.php
php复制namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\CustomerService;
use Illuminate\Http\Request;
class CustomerController extends Controller
{
public function __construct(private CustomerService $service) {}
public function index(Request $request)
{
$validated = $request->validate([
'page' => 'integer|min:1',
'search' => 'string|max:255',
'status' => 'in:active,inactive,blacklisted'
]);
return response()->json($this->service->paginate($validated));
}
}
CustomerService.php
php复制namespace App\Services;
use App\Models\Customer;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
class CustomerService
{
public function paginate(array $params)
{
$cacheKey = "customers:page:{$params['page']}:search:{$params['search'] ?? ''}";
return Cache::remember($cacheKey, 3600, function () use ($params) {
$query = Customer::with('user:id,name')
->when($params['search'], fn($q) => $q->where('name', 'like', "%{$params['search']}%"))
->when($params['status'], fn($q) => $q->where('status', $params['status']))
->orderBy('created_at', 'desc');
return $query->paginate(15, ['id', 'name', 'company', 'status', 'created_at']);
});
}
}
3.4 认证实现
使用Laravel Sanctum实现Token认证:
AuthController.php
php复制public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required'
]);
if (!Auth::attempt($credentials)) {
return response()->json(['message' => '账号或密码错误'], 401);
}
$token = $request->user()->createToken('crm-token')->plainTextToken;
return response()->json(['token' => $token]);
}
路由保护
php复制Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('customers', CustomerController::class);
});
4. 前端实现
4.1 项目初始化
使用Vite创建Vue3项目:
bash复制npm create vite@latest crm-frontend -- --template vue
cd crm-frontend
npm install element-plus pinia vue-router axios
4.2 API请求封装
utils/request.js
javascript复制import axios from 'axios';
import { ElMessage } from 'element-plus';
import router from '@/router';
const request = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 5000
});
request.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
request.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
router.push('/login');
ElMessage.error('登录已过期,请重新登录');
}
return Promise.reject(error);
}
);
export default request;
4.3 客户列表组件
CustomerList.vue
vue复制<template>
<div class="customer-list">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm">
<el-form-item label="关键词">
<el-input v-model="searchForm.keyword" placeholder="客户名称/公司" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable>
<el-option label="活跃" value="active" />
<el-option label="非活跃" value="inactive" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card">
<el-table :data="customers.data" stripe border v-loading="loading">
<el-table-column prop="name" label="客户名称" />
<el-table-column prop="company" label="所属公司" />
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'info'">
{{ row.status === 'active' ? '活跃' : '非活跃' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180" />
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
v-model:current-page="customers.current_page"
:page-size="customers.per_page"
:total="customers.total"
layout="total, prev, pager, next"
@current-change="fetchCustomers"
/>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { getCustomers, deleteCustomer } from '@/api/customer';
import { ElMessage, ElMessageBox } from 'element-plus';
const loading = ref(false);
const customers = ref({ data: [], current_page: 1, per_page: 15, total: 0 });
const searchForm = reactive({ keyword: '', status: '' });
const fetchCustomers = async () => {
loading.value = true;
try {
const res = await getCustomers({
page: customers.value.current_page,
search: searchForm.keyword,
status: searchForm.status
});
customers.value = res;
} finally {
loading.value = false;
}
};
const handleSearch = () => {
customers.value.current_page = 1;
fetchCustomers();
};
const handleDelete = (id) => {
ElMessageBox.confirm('确定删除该客户吗?', '警告', { type: 'warning' })
.then(async () => {
await deleteCustomer(id);
ElMessage.success('删除成功');
fetchCustomers();
});
};
onMounted(() => fetchCustomers());
</script>
5. 系统部署与优化
5.1 部署方案
推荐使用Docker进行容器化部署:
docker-compose.yml
yaml复制version: '3.8'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on:
- mysql
- redis
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
depends_on:
- backend
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=secret
- MYSQL_DATABASE=crm
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7.0-alpine
volumes:
mysql_data:
5.2 性能优化
-
前端优化:
- 使用Vite的代码分割功能
- 实现组件懒加载
- 启用Gzip压缩
-
后端优化:
- 使用Redis缓存高频查询
- 数据库查询优化(索引、预加载)
- 启用OPcache加速PHP执行
-
安全加固:
- 实现CSRF防护
- 输入参数严格验证
- 敏感数据加密存储
6. 常见问题与解决方案
6.1 跨域问题
问题描述:前端访问后端API时出现CORS错误
解决方案:
- 在后端配置CORS中间件
- 确保Sanctum的
stateful域名配置正确 - 前端axios配置
withCredentials: true
6.2 Token失效处理
问题描述:Token过期后前端未自动跳转登录页
解决方案:
- 在axios响应拦截器中处理401错误
- 清除本地存储的Token
- 跳转到登录页面并显示提示信息
6.3 分页缓存问题
问题描述:带搜索条件的分页结果被错误缓存
解决方案:
- 将搜索条件作为缓存key的一部分
- 设置合理的缓存过期时间
- 数据变更时清除相关缓存
7. 扩展功能建议
-
客户画像分析:
- 集成数据分析工具
- 可视化客户行为数据
- 生成客户价值评估报告
-
自动化营销:
- 邮件营销模板
- 短信营销接口
- 营销活动效果追踪
-
移动端适配:
- 开发响应式布局
- 封装为PWA应用
- 开发原生App版本
在实际开发中,我发现Laravel11的Eloquent ORM与Vue3的响应式系统配合非常默契。通过合理设计API响应结构,可以大大减少前端处理数据的复杂度。例如,在后端返回分页数据时保持结构一致,前端就可以复用相同的分页处理逻辑。