简介:这是一套基于PHP Laravel框架开发的开源OA协同办公平台Gdoo,专为食品行业生产型与贸易企业定制,聚焦销售管理、业绩分析与业务执行提效,同时覆盖日程安排、进销存、营销管理、简易生产计划及客户自主下单等核心场景,适合中高级PHP开发者二次开发或企业级应用快速落地。资源包共1390个文件,主体为906个PHP后端逻辑文件、126个JS前端交互脚本、121个PNG图标资源及62个CSS样式文件,辅以HTML、Vue、SQL等配套文件,整体压缩包仅14.57MB,轻量但功能完备。目前已有341人学习下载。用户可直接部署运行,获得含模型字段自定义、跨表映射、视图/流程/权限管理在内的完整低代码扩展能力,以及销售团队分级管控、客户任务进度统计等垂直业务模块,预览可见ag-grid、Layui、Bootstrap等主流UI组件集成,开箱即用且易于深度定制。
1. 这不是又一个“带审批流的后台模板”:Laravel驱动的OA系统,核心在业务域建模与运行时自定义能力
很多开发者看到“Laravel OA平台”第一反应是:又一个CRUD堆出来的管理后台,顶多加个流程图拖拽。但真正落地过中型企业协同场景的人清楚——销售线索漏斗怎么和日程自动联动?进销存单据状态变更如何触发多维度数据分析看板刷新?当财务要求把“采购入库单”字段顺序重排、校验规则加正则、导出Excel模板换列宽,而IT部门不能停服改代码时,问题就不再是“能不能做”,而是“改得快不快、稳不稳、谁来改”。这个基于Laravel的开源OA系统,本质是一套可运行时编排的领域模型引擎:它用Eloquent关系映射销售、库存、日程等实体,用Service Provider动态注册业务策略,用Blade组件+Livewire实现无刷新表单重构,最终让非开发人员能通过可视化界面调整字段、流程、报表维度。适合已有Laravel团队想快速交付定制化协同系统,或需要将现有进销存/CRM模块嵌入统一工作台的技术负责人。
2. 用Laravel Eloquent构建可扩展的业务实体层:销售、进销存、日程三域统一建模
2.1 为什么不用单表继承,而用多态关联+策略模式解耦核心域
Laravel默认的morphTo虽能实现“一个审批单关联销售单或采购单”,但会导致查询性能陡降(需JOIN多张表)且无法复用域逻辑。本方案采用显式多态关联+Domain Service分发:
SaleOrder、PurchaseOrder、InventoryReceipt各自独立建模,均实现HasWorkflow接口WorkflowInstance表只存subject_type(如App\Models\SaleOrder)和subject_id,不存业务字段- 状态变更时,
WorkflowService::handleTransition()根据subject_type动态解析对应Service类(如SaleOrderWorkflowService),调用其onApproved()方法执行销售域特有逻辑(如更新客户等级、触发邮件通知)
// app/Services/WorkflowService.php public function handleTransition(string $subjectType, int $subjectId, string $transition): void { $subject = app($subjectType)::findOrFail($subjectId); $serviceClass = str_replace('Models\\', 'Services\\', $subjectType) . 'WorkflowService'; app($serviceClass)->{$transition}($subject); // 动态调用 onApproved/onRejected }提示:
$subjectType必须为完整命名空间字符串,避免反射调用安全风险;生产环境建议用class_exists()校验后再实例化。
2.2 进销存核心:用库存事务表(inventory_transactions)替代传统“期初+本期=期末”硬编码
传统进销存常把库存计算写死在控制器里,导致“采购入库”“销售出库”“盘点损益”逻辑散落各处。本方案强制所有库存变动走InventoryTransaction模型:
| 字段 | 类型 | 说明 |
|---|---|---|
type | enum('purchase_in','sale_out','adjustment') | 事务类型,不可扩展为字符串防注入 |
reference_id | unsignedBigInteger | 关联单据ID(如purchase_orders.id) |
reference_type | varchar(50) | 关联模型类名(如App\Models\PurchaseOrder) |
quantity_delta | decimal(10,2) | 变动数量(正为入库,负为出库) |
warehouse_id | unsignedBigInteger | 仓库ID,支持多仓 |
// app/Models/InventoryTransaction.php protected $casts = [ 'quantity_delta' => 'decimal:2', ]; public function warehouse() { return $this->belongsTo(Warehouse::class); } // 批量入库时自动创建事务记录 public static function createFromPurchaseOrder(PurchaseOrder $order): void { foreach ($order->items as $item) { self::create([ 'type' => 'purchase_in', 'reference_id' => $order->id, 'reference_type' => PurchaseOrder::class, 'quantity_delta' => $item->quantity, 'warehouse_id' => $item->warehouse_id, ]); } }2.2.1 实时库存计算:用数据库视图替代应用层SUM()
为避免高并发下SELECT SUM(quantity_delta)锁表,创建MySQL视图:
CREATE VIEW current_inventory AS SELECT warehouse_id, product_id, SUM(quantity_delta) as available_quantity FROM inventory_transactions GROUP BY warehouse_id, product_id;Laravel中直接查询视图:
$stock = DB::table('current_inventory') ->where('warehouse_id', 1) ->where('product_id', 101) ->value('available_quantity'); // 返回decimal值,非字符串注意:视图在MySQL 5.7+支持,但需确保
inventory_transactions表有复合索引(warehouse_id, product_id),否则GROUP BY性能极差。
2.3 日程安排与销售线索的双向绑定:用事件驱动替代硬引用
销售员新建日程时,系统需自动关联其负责的客户线索;线索状态变更为“已签约”时,应删除所有待办日程。若用外键硬关联,会导致Schedules表膨胀且难以维护。本方案用Laravel事件总线:
// app/Events/SaleLeadStatusChanged.php class SaleLeadStatusChanged { public function __construct(public SaleLead $lead, public string $oldStatus, public string $newStatus) {} } // app/Listeners/CancelRelatedSchedules.php class CancelRelatedSchedules { public function handle(SaleLeadStatusChanged $event): void { if ($event->newStatus === 'won') { Schedule::where('related_type', SaleLead::class) ->where('related_id', $event->lead->id) ->update(['status' => 'cancelled']); } } }注册监听器到app/Providers/EventServiceProvider.php:
protected $listen = [ SaleLeadStatusChanged::class => [ CancelRelatedSchedules::class, SendWinNotification::class, // 可扩展其他业务 ], ];3. 自定义能力落地:用JSON Schema驱动表单、流程、报表三大可配置模块
3.1 表单自定义:用JSON Schema生成Blade组件,而非拖拽式前端框架
市面上多数OA用Vue拖拽生成表单,但后端校验、权限控制、数据持久化仍需手写。本方案将Schema定义与Laravel验证器深度绑定:
// resources/schemas/sale_order.json { "title": "销售订单", "type": "object", "properties": { "customer_id": { "type": "integer", "x-component": "select", "x-options": {"model": "App\\Models\\Customer", "label": "name", "value": "id"} }, "expected_delivery_date": { "type": "string", "format": "date", "x-component": "date-picker" }, "discount_rate": { "type": "number", "minimum": 0, "maximum": 100, "x-component": "slider", "x-step": 0.1 } }, "required": ["customer_id"] }后端动态生成验证规则:
// app/Http/Controllers/CustomFormController.php public function store(Request $request, string $schemaName) { $schema = json_decode(file_get_contents(resource_path("schemas/{$schemaName}.json")), true); $rules = $this->buildValidationRules($schema['properties']); $validated = $request->validate($rules); // ... 保存到对应模型 } private function buildValidationRules(array $properties): array { $rules = []; foreach ($properties as $field => $config) { $rule = []; if (isset($config['type'])) { switch ($config['type']) { case 'integer': $rule[] = 'integer'; break; case 'number': $rule[] = 'numeric'; break; case 'string': $rule[] = 'string'; break; } } if (isset($config['minimum'])) $rule[] = "min:{$config['minimum']}"; if (isset($config['maximum'])) $rule[] = "max:{$config['maximum']}"; if (in_array($field, $schema['required'] ?? [])) $rule[] = 'required'; $rules[$field] = $rule; } return $rules; }3.2 流程引擎:用状态机DSL定义审批路径,避免硬编码if-else
不采用Activiti等重型BPMN引擎,而是用轻量级状态机配置:
// config/workflows/sale_approval.php return [ 'initial_state' => 'draft', 'states' => [ 'draft' => ['actions' => ['submit']], 'pending_review' => ['actions' => ['approve', 'reject']], 'approved' => ['actions' => ['ship']], 'rejected' => ['actions' => ['resubmit']], ], 'transitions' => [ 'submit' => ['from' => 'draft', 'to' => 'pending_review'], 'approve' => ['from' => 'pending_review', 'to' => 'approved'], 'reject' => ['from' => 'pending_review', 'to' => 'rejected'], 'ship' => ['from' => 'approved', 'to' => 'shipped'], 'resubmit' => ['from' => 'rejected', 'to' => 'draft'], ], 'guards' => [ 'approve' => 'App\\Policies\\SaleOrderPolicy@canApprove', 'reject' => 'App\\Policies\\SaleOrderPolicy@canReject', ], ];状态变更时自动调用Guard:
// app/Services/WorkflowService.php public function transition(string $workflow, int $id, string $action): bool { $config = config("workflows.{$workflow}"); $model = $this->getModelByWorkflow($workflow, $id); // 校验权限 $guard = $config['guards'][$action] ?? null; if ($guard && !app()->call($guard, ['model' => $model])) { throw new UnauthorizedException("No permission to {$action}"); } // 更新状态 $model->status = $config['transitions'][$action]['to']; $model->save(); return true; }3.3 数据分析看板:用Eloquent Query Builder + Blade组件实现低代码报表
拒绝将SQL写死在控制器里。报表配置存于数据库report_configs表:
| 字段 | 类型 | 说明 |
|---|---|---|
name | varchar(100) | 报表名称(如“月度销售TOP10”) |
query_json | json | 查询定义(见下文) |
columns | json | 列配置(含格式化函数) |
query_json示例:
{ "model": "App\\Models\\SaleOrder", "select": ["customer.name as customer_name", "SUM(total_amount) as amount"], "joins": [{"table": "customers", "on": "sale_orders.customer_id = customers.id"}], "where": [{"column": "created_at", "operator": ">=", "value": "2024-01-01"}], "groupBy": ["customer.name"] }动态执行查询:
// app/Services/ReportService.php public function execute(array $config): Collection { $builder = app($config['model'])->newQuery(); if (!empty($config['select'])) { $builder->selectRaw(implode(', ', $config['select'])); } if (!empty($config['joins'])) { foreach ($config['joins'] as $join) { $builder->join($join['table'], \DB::raw($join['on'])); } } if (!empty($config['where'])) { foreach ($config['where'] as $cond) { $builder->where($cond['column'], $cond['operator'], $cond['value']); } } if (!empty($config['groupBy'])) { $builder->groupBy($config['groupBy']); } return $builder->get(); }4. 部署与性能关键:用Redis队列处理数据分析,用OPcache优化自定义Schema解析
4.1 避免报表查询阻塞Web进程:将慢查询移至Laravel Horizon队列
当用户点击“导出年度销售报表”时,若直接执行SELECT ... GROUP BY可能耗时数秒。必须异步化:
// app/Jobs/GenerateSalesReport.php class GenerateSalesReport implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( public int $userId, public array $filters, public string $exportFormat = 'xlsx' ) {} public function handle(): void { $data = app(ReportService::class)->executeForSales($this->filters); $filePath = storage_path("app/reports/sales_{$this->userId}_" . now()->timestamp . '.xlsx'); (new FastExcel($data))->export($filePath); // 发送下载链接邮件 Mail::to(User::find($this->userId))->send(new ReportReady($filePath)); } }配置Horizon监控队列:
# 启动Horizon php artisan horizon # 查看队列延迟(关键指标) redis-cli -p 6379 llen horizon:default提示:
FastExcel比PhpSpreadsheet内存占用低80%,适合大数据量导出;务必在config/horizon.php中设置memory_limit防止OOM。
4.2 加速JSON Schema解析:用OPcache预编译Schema文件
每次读取resources/schemas/*.json都需file_get_contents+json_decode,在高并发下成为瓶颈。启用OPcache并预加载:
// config/app.php 'providers' => [ // ... 其他Provider App\Providers\SchemaServiceProvider::class, // 新增 ], // app/Providers/SchemaServiceProvider.php public function register(): void { if (function_exists('opcache_compile_file')) { $schemaDir = resource_path('schemas'); foreach (glob("{$schemaDir}/*.json") as $file) { opcache_compile_file($file); // 预编译JSON文件为字节码 } } }验证OPcache是否生效:
# 查看缓存命中率(应>95%) php -r "print_r(opcache_get_status()['opcache_statistics']);"4.3 进销存高频操作优化:用Redis Pipeline批量更新库存
当一笔采购单含50个商品时,逐条INSERT INTO inventory_transactions会触发50次网络往返。改用Pipeline:
// app/Services/InventoryService.php public function bulkCreateTransactions(array $transactions): void { $pipeline = Redis::pipeline(); foreach ($transactions as $tx) { $pipeline->lpush('inventory_queue', json_encode($tx)); // 先入队 } $pipeline->execute(); // 一次网络请求 // 后台Job消费队列 ProcessInventoryQueue::dispatch(); }后台Job原子化处理:
// app/Jobs/ProcessInventoryQueue.php public function handle(): void { $batch = Redis::lrange('inventory_queue', 0, 99); // 每次取100条 if (empty($batch)) return; DB::transaction(function () use ($batch) { foreach ($batch as $json) { $data = json_decode($json, true); InventoryTransaction::create($data); } Redis::ltrim('inventory_queue', count($batch), -1); // 删除已处理 }); }5. 安全与审计:用Laravel Policy + 数据变更日志保障OA系统合规性
5.1 销售数据权限隔离:用Policy实现“销售员只能看自己客户”
不依赖简单WHERE user_id = ?,而是用Laravel Policy的before()方法全局拦截:
// app/Policies/SaleLeadPolicy.php public function before(User $user, $ability): bool|null { if ($user->hasRole('admin')) { return true; // 管理员跳过所有检查 } return null; // 继续执行具体方法 } public function view(User $user, SaleLead $lead): bool { return $lead->owner_id === $user->id || $user->managedTeams()->where('team_id', $lead->team_id)->exists(); }在控制器中强制调用:
// app/Http/Controllers/SaleLeadController.php public function index() { $this->authorize('viewAny', SaleLead::class); return SaleLead::with('customer')->where('owner_id', auth()->id())->get(); }5.2 关键操作留痕:用DB监听器记录进销存变动详情
所有inventory_transactions插入必须记录操作人、IP、UA:
// app/Providers/AppServiceProvider.php public function boot(): void { InventoryTransaction::created(function (InventoryTransaction $tx) { ActivityLog::create([ 'log_name' => 'inventory', 'description' => "库存变动:{$tx->type} {$tx->quantity_delta}", 'subject_id' => $tx->id, 'subject_type' => InventoryTransaction::class, 'causer_id' => auth()->id() ?: 0, 'causer_type' => User::class, 'properties' => [ 'ip' => request()->ip(), 'user_agent' => request()->userAgent(), 'reference' => $tx->reference_type . '#' . $tx->reference_id, ], ]); }); }查询日志时还原上下文:
// 获取某笔采购入库的完整链路 $log = ActivityLog::where('log_name', 'inventory') ->whereHas('subject', function ($q) { $q->where('type', 'purchase_in')->where('reference_id', 123); }) ->with(['causer', 'subject']) ->first(); // 输出:[2024-03-15 14:22] 张三(销售部)在192.168.1.100操作:采购入库 100件(采购单#123)5.3 OA审批防重复提交:用Laravel Throttle Requests中间件限制状态变更频率
避免用户狂点“同意”按钮导致多次触发onApproved():
// app/Http/Controllers/WorkflowController.php public function approve(Request $request, int $id) { // 5秒内同一审批单只允许1次操作 $key = 'workflow_approve_' . $id . '_' . auth()->id(); if (Cache::has($key)) { abort(429, '操作过于频繁,请稍后再试'); } Cache::put($key, true, 5); app(WorkflowService::class)->transition('sale_approval', $id, 'approve'); return response()->noContent(); }或全局中间件(推荐):
// app/Http/Middleware/ThrottleWorkflow.php public function handle(Request $request, Closure $next) { $key = 'workflow_' . $request->route('id') . '_' . auth()->id(); $maxAttempts = 1; $decayMinutes = 1; if (RateLimiter::tooManyAttempts($key, $maxAttempts)) { throw new ThrottleRequestsException( '操作过于频繁', $request->getUri(), $this->getTimeUntilNextAttempt($key, $decayMinutes) ); } RateLimiter::hit($key, $decayMinutes); return $next($request); }注册到app/Http/Kernel.php:
'workflow' => \App\Http\Middleware\ThrottleWorkflow::class,然后在路由中使用:
Route::middleware('workflow')->post('/workflow/{id}/approve', [WorkflowController::class, 'approve']);本文还有配套的精品资源,点击获取