feat: 第三阶段业务模块(服务产康/月嫂/进销存)

- 新增3个migration(15张表): service/nanny/inventory
- 新增15个Model + 13个Controller
- 新增55条API路由(总计212条)
- 新增3个前端API模块 + 11个管理页面
- vite build验证通过
This commit is contained in:
li
2026-03-13 20:29:45 +08:00
parent 16bcd9bcf0
commit e7d4d9b414
46 changed files with 2573 additions and 0 deletions
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Inventory\Inventory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class InventoryController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Inventory::query()->with(['warehouse','material']);
$query->when($request->warehouse_id, fn($q, $v) => $q->where('warehouse_id', $v));
$query->when($request->material_id, fn($q, $v) => $q->where('material_id', $v));
return $this->paginate($query->paginate($request->input('per_page', 50)));
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Inventory\Material;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class MaterialController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Material::query();
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
$query->when($request->category, fn($q, $v) => $q->where('category', $v));
$query->when($request->code, fn($q, $v) => $q->where('code', 'like', "%{$v}%"));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$v = $request->validate(['name'=>'required|string|max:100','code'=>'nullable|string|max:50','category'=>'nullable|string|max:50','unit'=>'nullable|string|max:20','spec'=>'nullable|string|max:100','min_stock'=>'sometimes|integer|min:0','shelf_life_days'=>'nullable|integer|min:1','status'=>'sometimes|integer|in:0,1']);
return $this->success(Material::create($v));
}
public function show(Material $material): JsonResponse { return $this->success($material); }
public function update(Request $request, Material $material): JsonResponse { $v = $request->validate(['name'=>'sometimes|string|max:100','code'=>'nullable|string|max:50','category'=>'nullable|string|max:50','unit'=>'nullable|string|max:20','spec'=>'nullable|string|max:100','min_stock'=>'sometimes|integer|min:0','shelf_life_days'=>'nullable|integer|min:1','status'=>'sometimes|integer|in:0,1']); $material->update($v); return $this->success($material); }
public function destroy(Material $material): JsonResponse { $material->delete(); return $this->success(null); }
}
@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Inventory\PurchaseOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PurchaseOrderController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = PurchaseOrder::query()->with(['supplier','auditor']);
$query->when($request->order_no, fn($q, $v) => $q->where('order_no', 'like', "%{$v}%"));
$query->when($request->supplier_id, fn($q, $v) => $q->where('supplier_id', $v));
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$v = $request->validate(['order_no'=>'required|string|max:50|unique:purchase_orders','supplier_id'=>'nullable|exists:suppliers,id','total_amount'=>'nullable|numeric|min:0','items'=>'nullable|array','remark'=>'nullable|string']);
$v['status'] = 0; $v['created_by'] = auth()->id();
return $this->success(PurchaseOrder::create($v));
}
public function show(PurchaseOrder $purchaseOrder): JsonResponse { return $this->success($purchaseOrder->load(['supplier','auditor','creator'])); }
public function update(Request $request, PurchaseOrder $purchaseOrder): JsonResponse
{
if ($purchaseOrder->status >= 2) return $this->error('已审批的采购单不可修改', 40001);
$v = $request->validate(['supplier_id'=>'nullable|exists:suppliers,id','total_amount'=>'nullable|numeric|min:0','items'=>'nullable|array','remark'=>'nullable|string']);
$purchaseOrder->update($v);
return $this->success($purchaseOrder);
}
public function audit(Request $request, PurchaseOrder $purchaseOrder): JsonResponse
{
if ($purchaseOrder->status >= 2) return $this->error('已审批', 40001);
$v = $request->validate(['status'=>'required|integer|in:2,4']);
$purchaseOrder->update(['status'=>$v['status'],'audit_user_id'=>auth()->id(),'audit_at'=>now()]);
return $this->success($purchaseOrder);
}
public function destroy(PurchaseOrder $purchaseOrder): JsonResponse
{
if ($purchaseOrder->status >= 2) return $this->error('已审批不可删除', 40001);
$purchaseOrder->delete(); return $this->success(null);
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Inventory\StockMovement;
use App\Models\Inventory\Inventory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class StockMovementController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = StockMovement::query()->with('warehouse');
$query->when($request->warehouse_id, fn($q, $v) => $q->where('warehouse_id', $v));
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$v = $request->validate([
'warehouse_id'=>'required|exists:warehouses,id','movement_no'=>'required|string|max:50|unique:stock_movements',
'type'=>'required|integer|in:1,2,3,4','direction'=>'nullable|integer|in:1,2',
'items'=>'nullable|array','related_order'=>'nullable|string|max:50','remark'=>'nullable|string',
]);
$v['status'] = 0; $v['operator_id'] = auth()->id();
return $this->success(StockMovement::create($v));
}
public function show(StockMovement $stockMovement): JsonResponse { return $this->success($stockMovement->load(['warehouse','operator'])); }
public function confirm(StockMovement $stockMovement): JsonResponse
{
if ($stockMovement->status === 1) return $this->error('已确认', 40001);
$stockMovement->update(['status' => 1]);
// 更新库存
if ($stockMovement->items) {
foreach ($stockMovement->items as $item) {
$inv = Inventory::firstOrCreate(
['warehouse_id'=>$stockMovement->warehouse_id,'material_id'=>$item['material_id'],'batch_no'=>$item['batch_no']??null],
['quantity'=>0]
);
$qty = $item['quantity'] ?? 0;
$inv->quantity = $stockMovement->direction === 1 ? $inv->quantity + $qty : $inv->quantity - $qty;
$inv->save();
}
}
return $this->success($stockMovement);
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Inventory\Supplier;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SupplierController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Supplier::query();
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$v = $request->validate(['name'=>'required|string|max:100','contact'=>'nullable|string|max:50','phone'=>'nullable|string|max:20','address'=>'nullable|string|max:255','bank_info'=>'nullable|string|max:255','status'=>'sometimes|integer|in:0,1']);
return $this->success(Supplier::create($v));
}
public function show(Supplier $supplier): JsonResponse { return $this->success($supplier); }
public function update(Request $request, Supplier $supplier): JsonResponse { $v = $request->validate(['name'=>'sometimes|string|max:100','contact'=>'nullable|string|max:50','phone'=>'nullable|string|max:20','address'=>'nullable|string|max:255','bank_info'=>'nullable|string|max:255','status'=>'sometimes|integer|in:0,1']); $supplier->update($v); return $this->success($supplier); }
public function destroy(Supplier $supplier): JsonResponse { $supplier->delete(); return $this->success(null); }
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Inventory\Warehouse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class WarehouseController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Warehouse::query()->with('manager');
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$v = $request->validate(['name'=>'required|string|max:50','manager_id'=>'nullable|exists:users,id','status'=>'sometimes|integer|in:0,1']);
return $this->success(Warehouse::create($v));
}
public function show(Warehouse $warehouse): JsonResponse { return $this->success($warehouse->load('manager')); }
public function update(Request $request, Warehouse $warehouse): JsonResponse { $v = $request->validate(['name'=>'sometimes|string|max:50','manager_id'=>'nullable|exists:users,id','status'=>'sometimes|integer|in:0,1']); $warehouse->update($v); return $this->success($warehouse); }
public function destroy(Warehouse $warehouse): JsonResponse { $warehouse->delete(); return $this->success(null); }
}
@@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\Admin\Nanny;
use App\Http\Controllers\Controller;
use App\Models\Nanny\Nanny;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class NannyController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Nanny::query();
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
$query->when($request->level, fn($q, $v) => $q->where('level', $v));
$query->when($request->status, fn($q, $v) => $q->where('status', $v));
$query->when($request->cooperation_type, fn($q, $v) => $q->where('cooperation_type', $v));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name'=>'required|string|max:50','phone'=>'nullable|string|max:20','id_card'=>'nullable|string|max:20',
'level'=>'nullable|integer|in:1,2,3,4','skills'=>'nullable|array','experience_years'=>'nullable|integer|min:0',
'health_cert'=>'nullable|string|max:255','cooperation_type'=>'nullable|integer|in:1,2,3',
'base_salary'=>'nullable|numeric|min:0','introduction'=>'nullable|string','status'=>'sometimes|integer|in:1,2,3,4',
]);
return $this->success(Nanny::create($validated));
}
public function show(Nanny $nanny): JsonResponse { return $this->success($nanny->load(['orders.customer','schedules'])); }
public function update(Request $request, Nanny $nanny): JsonResponse
{
$validated = $request->validate([
'name'=>'sometimes|string|max:50','phone'=>'nullable|string|max:20','level'=>'nullable|integer|in:1,2,3,4',
'skills'=>'nullable|array','experience_years'=>'nullable|integer|min:0','cooperation_type'=>'nullable|integer|in:1,2,3',
'base_salary'=>'nullable|numeric|min:0','introduction'=>'nullable|string','status'=>'sometimes|integer|in:1,2,3,4',
]);
$nanny->update($validated);
return $this->success($nanny);
}
public function destroy(Nanny $nanny): JsonResponse
{
if ($nanny->status === 2) return $this->error('服务中的月嫂无法删除', 40001);
$nanny->delete();
return $this->success(null);
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers\Admin\Nanny;
use App\Http\Controllers\Controller;
use App\Models\Nanny\NannyOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class NannyOrderController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = NannyOrder::query()->with(['customer','nanny']);
$query->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v));
$query->when($request->nanny_id, fn($q, $v) => $q->where('nanny_id', $v));
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'customer_id'=>'required|exists:customers,id','nanny_id'=>'nullable|exists:nannies,id',
'order_no'=>'required|string|max:50|unique:nanny_orders','service_type'=>'nullable|integer|in:1,2',
'start_date'=>'nullable|date','end_date'=>'nullable|date|after_or_equal:start_date',
'days'=>'nullable|integer|min:1','price'=>'nullable|numeric|min:0','remark'=>'nullable|string',
]);
$validated['status'] = 0;
$validated['created_by'] = auth()->id();
return $this->success(NannyOrder::create($validated));
}
public function show(NannyOrder $nannyOrder): JsonResponse { return $this->success($nannyOrder->load(['customer','nanny','reviews'])); }
public function update(Request $request, NannyOrder $nannyOrder): JsonResponse
{
if ($nannyOrder->status >= 4) return $this->error('已完成/取消的订单不可修改', 40001);
$validated = $request->validate([
'nanny_id'=>'nullable|exists:nannies,id','start_date'=>'nullable|date','end_date'=>'nullable|date',
'days'=>'nullable|integer|min:1','price'=>'nullable|numeric|min:0','status'=>'sometimes|integer|in:0,1,2,3,4,5',
'match_candidates'=>'nullable|array','remark'=>'nullable|string',
]);
$nannyOrder->update($validated);
if (isset($validated['nanny_id']) && $validated['nanny_id']) {
$nannyOrder->nanny->update(['status' => 2]);
}
return $this->success($nannyOrder);
}
public function destroy(NannyOrder $nannyOrder): JsonResponse
{
if ($nannyOrder->status >= 2) return $this->error('已签约后不可删除', 40001);
$nannyOrder->delete();
return $this->success(null);
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Admin\Nanny;
use App\Http\Controllers\Controller;
use App\Models\Nanny\NannySchedule;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class NannyScheduleController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = NannySchedule::query()->with(['nanny','order']);
$query->when($request->nanny_id, fn($q, $v) => $q->where('nanny_id', $v));
$query->when($request->start_date, fn($q, $v) => $q->where('schedule_date', '>=', $v));
$query->when($request->end_date, fn($q, $v) => $q->where('schedule_date', '<=', $v));
return $this->paginate($query->orderBy('schedule_date')->paginate($request->input('per_page', 31)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'nanny_id'=>'required|exists:nannies,id','nanny_order_id'=>'nullable|exists:nanny_orders,id',
'schedule_date'=>'required|date','type'=>'nullable|integer|in:1,2,3','remark'=>'nullable|string|max:255',
]);
return $this->success(NannySchedule::create($validated));
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\Admin\Service;
use App\Http\Controllers\Controller;
use App\Models\Service\ServiceExecution;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServiceExecutionController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = ServiceExecution::query()->with(['order','serviceItem','customer','technician']);
$query->when($request->service_order_id, fn($q, $v) => $q->where('service_order_id', $v));
$query->when($request->technician_id, fn($q, $v) => $q->where('technician_id', $v));
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'service_order_id'=>'required|exists:service_orders,id','service_item_id'=>'required|exists:service_items,id',
'customer_id'=>'required|exists:customers,id','technician_id'=>'nullable|exists:users,id',
'scheduled_at'=>'nullable|date','remark'=>'nullable|string',
]);
$validated['status'] = 0;
return $this->success(ServiceExecution::create($validated));
}
public function start(ServiceExecution $serviceExecution): JsonResponse
{
if ($serviceExecution->status !== 0) return $this->error('只有待执行可开始', 40001);
$serviceExecution->update(['status'=>1,'started_at'=>now()]);
return $this->success($serviceExecution);
}
public function finish(ServiceExecution $serviceExecution): JsonResponse
{
if ($serviceExecution->status !== 1) return $this->error('只有执行中可完成', 40001);
$serviceExecution->update(['status'=>2,'ended_at'=>now()]);
return $this->success($serviceExecution);
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Admin\Service;
use App\Http\Controllers\Controller;
use App\Models\Service\ServiceItem;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServiceItemController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = ServiceItem::query();
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
$query->when($request->category, fn($q, $v) => $q->where('category', $v));
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'category' => 'nullable|integer|in:1,2,3,4',
'price' => 'required|numeric|min:0',
'duration' => 'nullable|integer|min:1',
'description' => 'nullable|string',
'image' => 'nullable|string|max:255',
'status' => 'sometimes|integer|in:0,1',
]);
return $this->success(ServiceItem::create($validated));
}
public function show(ServiceItem $serviceItem): JsonResponse { return $this->success($serviceItem); }
public function update(Request $request, ServiceItem $serviceItem): JsonResponse
{
$validated = $request->validate([
'name' => 'sometimes|string|max:100',
'category' => 'nullable|integer|in:1,2,3,4',
'price' => 'sometimes|numeric|min:0',
'duration' => 'nullable|integer|min:1',
'description' => 'nullable|string',
'image' => 'nullable|string|max:255',
'status' => 'sometimes|integer|in:0,1',
]);
$serviceItem->update($validated);
return $this->success($serviceItem);
}
public function destroy(ServiceItem $serviceItem): JsonResponse { $serviceItem->delete(); return $this->success(null); }
}
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Admin\Service;
use App\Http\Controllers\Controller;
use App\Models\Service\ServiceOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServiceOrderController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = ServiceOrder::query()->with(['customer']);
$query->when($request->order_no, fn($q, $v) => $q->where('order_no', 'like', "%{$v}%"));
$query->when($request->customer_id, fn($q, $v) => $q->where('customer_id', $v));
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'customer_id'=>'required|exists:customers,id','order_no'=>'required|string|max:50|unique:service_orders',
'type'=>'nullable|integer|in:1,2,3,4','items'=>'nullable|array',
'total_amount'=>'required|numeric|min:0','discount_amount'=>'sometimes|numeric|min:0','actual_amount'=>'required|numeric|min:0',
'pay_method'=>'nullable|integer|in:1,2,3,4','remark'=>'nullable|string',
]);
$validated['status'] = 0;
$validated['created_by'] = auth()->id();
return $this->success(ServiceOrder::create($validated));
}
public function show(ServiceOrder $serviceOrder): JsonResponse { return $this->success($serviceOrder->load(['customer','executions.serviceItem','executions.technician'])); }
public function update(Request $request, ServiceOrder $serviceOrder): JsonResponse
{
if ($serviceOrder->status >= 2) return $this->error('已完成/取消的订单不可修改', 40001);
$validated = $request->validate(['items'=>'nullable|array','total_amount'=>'sometimes|numeric|min:0','discount_amount'=>'sometimes|numeric|min:0','actual_amount'=>'sometimes|numeric|min:0','remark'=>'nullable|string']);
$serviceOrder->update($validated);
return $this->success($serviceOrder);
}
public function destroy(ServiceOrder $serviceOrder): JsonResponse
{
if ($serviceOrder->status >= 1) return $this->error('已付款的订单不可删除', 40001);
$serviceOrder->delete();
return $this->success(null);
}
public function pay(Request $request, ServiceOrder $serviceOrder): JsonResponse
{
if ($serviceOrder->status !== 0) return $this->error('该订单不可付款', 40001);
$validated = $request->validate(['pay_method'=>'required|integer|in:1,2,3,4']);
$serviceOrder->update(['status'=>1,'pay_method'=>$validated['pay_method'],'paid_at'=>now()]);
return $this->success($serviceOrder);
}
public function complete(ServiceOrder $serviceOrder): JsonResponse
{
if ($serviceOrder->status !== 1) return $this->error('只有已付款的订单可完成', 40001);
$serviceOrder->update(['status'=>2]);
return $this->success($serviceOrder);
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Admin\Service;
use App\Http\Controllers\Controller;
use App\Models\Service\ServicePackage;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServicePackageController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = ServicePackage::query();
$query->when($request->name, fn($q, $v) => $q->where('name', 'like', "%{$v}%"));
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate(['name'=>'required|string|max:100','items'=>'nullable|array','price'=>'required|numeric|min:0','validity_days'=>'nullable|integer|min:1','status'=>'sometimes|integer|in:0,1']);
return $this->success(ServicePackage::create($validated));
}
public function show(ServicePackage $servicePackage): JsonResponse { return $this->success($servicePackage); }
public function update(Request $request, ServicePackage $servicePackage): JsonResponse
{
$validated = $request->validate(['name'=>'sometimes|string|max:100','items'=>'nullable|array','price'=>'sometimes|numeric|min:0','validity_days'=>'nullable|integer|min:1','status'=>'sometimes|integer|in:0,1']);
$servicePackage->update($validated);
return $this->success($servicePackage);
}
public function destroy(ServicePackage $servicePackage): JsonResponse { $servicePackage->delete(); return $this->success(null); }
}
@@ -0,0 +1,13 @@
<?php
namespace App\Models\Inventory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Inventory extends Model
{
const CREATED_AT = null;
protected $table = 'inventories';
protected $fillable = ['warehouse_id','material_id','quantity','batch_no','expire_date'];
protected function casts(): array { return ['quantity'=>'decimal:2','expire_date'=>'date']; }
public function warehouse(): BelongsTo { return $this->belongsTo(Warehouse::class); }
public function material(): BelongsTo { return $this->belongsTo(Material::class); }
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models\Inventory;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
class Material extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','name','code','category','unit','spec','min_stock','shelf_life_days','status'];
protected function casts(): array { return ['min_stock'=>'integer','shelf_life_days'=>'integer','status'=>'integer']; }
}
@@ -0,0 +1,15 @@
<?php
namespace App\Models\Inventory;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PurchaseOrder extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','order_no','supplier_id','total_amount','status','items','audit_user_id','audit_at','remark','created_by'];
protected function casts(): array { return ['total_amount'=>'decimal:2','status'=>'integer','items'=>'array','audit_at'=>'datetime']; }
public function supplier(): BelongsTo { return $this->belongsTo(Supplier::class); }
public function auditor(): BelongsTo { return $this->belongsTo(User::class, 'audit_user_id'); }
public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); }
}
@@ -0,0 +1,14 @@
<?php
namespace App\Models\Inventory;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class StockMovement extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','warehouse_id','movement_no','type','direction','items','related_order','status','operator_id','remark'];
protected function casts(): array { return ['type'=>'integer','direction'=>'integer','items'=>'array','status'=>'integer']; }
public function warehouse(): BelongsTo { return $this->belongsTo(Warehouse::class); }
public function operator(): BelongsTo { return $this->belongsTo(User::class, 'operator_id'); }
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models\Inventory;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
class Supplier extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','name','contact','phone','address','bank_info','status'];
protected function casts(): array { return ['status'=>'integer']; }
}
@@ -0,0 +1,13 @@
<?php
namespace App\Models\Inventory;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Warehouse extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','name','manager_id','status'];
protected function casts(): array { return ['status'=>'integer']; }
public function manager(): BelongsTo { return $this->belongsTo(User::class, 'manager_id'); }
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Models\Nanny;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Nanny extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','name','phone','id_card','avatar','level','skills','experience_years','health_cert','cooperation_type','base_salary','introduction','status'];
protected function casts(): array { return ['level'=>'integer','skills'=>'array','experience_years'=>'integer','cooperation_type'=>'integer','base_salary'=>'decimal:2','status'=>'integer']; }
public function orders(): HasMany { return $this->hasMany(NannyOrder::class); }
public function schedules(): HasMany { return $this->hasMany(NannySchedule::class); }
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models\Nanny;
use App\Models\Crm\Customer;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class NannyOrder extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','customer_id','nanny_id','order_no','service_type','start_date','end_date','days','price','status','match_candidates','remark','created_by'];
protected function casts(): array { return ['service_type'=>'integer','start_date'=>'date','end_date'=>'date','days'=>'integer','price'=>'decimal:2','status'=>'integer','match_candidates'=>'array']; }
public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }
public function nanny(): BelongsTo { return $this->belongsTo(Nanny::class); }
public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); }
public function reviews(): HasMany { return $this->hasMany(NannyReview::class); }
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Models\Nanny;
use App\Models\Crm\Customer;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class NannyReview extends Model
{
const UPDATED_AT = null;
protected $fillable = ['nanny_order_id','customer_id','nanny_id','score','content'];
protected function casts(): array { return ['score'=>'integer']; }
public function order(): BelongsTo { return $this->belongsTo(NannyOrder::class, 'nanny_order_id'); }
public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }
public function nanny(): BelongsTo { return $this->belongsTo(Nanny::class); }
}
@@ -0,0 +1,12 @@
<?php
namespace App\Models\Nanny;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class NannySchedule extends Model
{
const UPDATED_AT = null;
protected $fillable = ['nanny_id','nanny_order_id','schedule_date','type','remark'];
protected function casts(): array { return ['schedule_date'=>'date','type'=>'integer']; }
public function nanny(): BelongsTo { return $this->belongsTo(Nanny::class); }
public function order(): BelongsTo { return $this->belongsTo(NannyOrder::class, 'nanny_order_id'); }
}
@@ -0,0 +1,15 @@
<?php
namespace App\Models\Service;
use App\Models\Crm\Customer;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ServiceExecution extends Model
{
protected $fillable = ['service_order_id','service_item_id','customer_id','technician_id','scheduled_at','started_at','ended_at','status','remark'];
protected function casts(): array { return ['scheduled_at'=>'datetime','started_at'=>'datetime','ended_at'=>'datetime','status'=>'integer']; }
public function order(): BelongsTo { return $this->belongsTo(ServiceOrder::class, 'service_order_id'); }
public function serviceItem(): BelongsTo { return $this->belongsTo(ServiceItem::class); }
public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }
public function technician(): BelongsTo { return $this->belongsTo(User::class, 'technician_id'); }
}
@@ -0,0 +1,10 @@
<?php
namespace App\Models\Service;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
class ServiceItem extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','name','category','price','duration','description','image','status'];
protected function casts(): array { return ['category'=>'integer','price'=>'decimal:2','duration'=>'integer','status'=>'integer']; }
}
@@ -0,0 +1,17 @@
<?php
namespace App\Models\Service;
use App\Models\Crm\Customer;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ServiceOrder extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','customer_id','order_no','type','items','total_amount','discount_amount','actual_amount','status','pay_method','paid_at','created_by'];
protected function casts(): array { return ['type'=>'integer','items'=>'array','total_amount'=>'decimal:2','discount_amount'=>'decimal:2','actual_amount'=>'decimal:2','status'=>'integer','pay_method'=>'integer','paid_at'=>'datetime']; }
public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }
public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); }
public function executions(): HasMany { return $this->hasMany(ServiceExecution::class); }
}
@@ -0,0 +1,10 @@
<?php
namespace App\Models\Service;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
class ServicePackage extends Model
{
use BelongsToStore;
protected $fillable = ['store_id','name','items','price','validity_days','status'];
protected function casts(): array { return ['items'=>'array','price'=>'decimal:2','validity_days'=>'integer','status'=>'integer']; }
}
@@ -0,0 +1,13 @@
<?php
namespace App\Models\Service;
use App\Models\Crm\Customer;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ServiceReview extends Model
{
const UPDATED_AT = null;
protected $fillable = ['service_order_id','service_execution_id','customer_id','score','content'];
protected function casts(): array { return ['score'=>'integer']; }
public function order(): BelongsTo { return $this->belongsTo(ServiceOrder::class, 'service_order_id'); }
public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }
}
@@ -0,0 +1,96 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// 服务项目
Schema::create('service_items', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('name', 100);
$table->tinyInteger('category')->nullable()->comment('1产康 2零售 3加餐 4其他');
$table->decimal('price', 10, 2);
$table->integer('duration')->nullable()->comment('服务时长(分钟)');
$table->text('description')->nullable();
$table->string('image', 255)->nullable();
$table->tinyInteger('status')->default(1);
$table->timestamps();
$table->index(['store_id', 'category']);
});
// 服务包
Schema::create('service_packages', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('name', 100);
$table->json('items')->nullable()->comment('包含项目与次数');
$table->decimal('price', 10, 2);
$table->integer('validity_days')->nullable()->comment('有效天数');
$table->tinyInteger('status')->default(1);
$table->timestamps();
});
// 服务订单
Schema::create('service_orders', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->foreignId('customer_id')->constrained();
$table->string('order_no', 50)->unique();
$table->tinyInteger('type')->nullable()->comment('1单项 2服务包 3零售 4加餐');
$table->json('items')->nullable()->comment('订单项明细');
$table->decimal('total_amount', 12, 2)->nullable();
$table->decimal('discount_amount', 12, 2)->default(0);
$table->decimal('actual_amount', 12, 2)->nullable();
$table->tinyInteger('status')->default(0)->comment('0待付 1已付 2已完成 3已取消 4已退');
$table->tinyInteger('pay_method')->nullable()->comment('1微信 2支付宝 3现金 4挂账');
$table->timestamp('paid_at')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
$table->index('store_id');
$table->index('customer_id');
});
// 服务执行记录
Schema::create('service_executions', function (Blueprint $table) {
$table->id();
$table->foreignId('service_order_id')->constrained()->cascadeOnDelete();
$table->foreignId('service_item_id')->constrained();
$table->foreignId('customer_id')->constrained();
$table->unsignedBigInteger('technician_id')->nullable()->comment('技师/产康师');
$table->timestamp('scheduled_at')->nullable()->comment('预约时间');
$table->timestamp('started_at')->nullable();
$table->timestamp('ended_at')->nullable();
$table->tinyInteger('status')->default(0)->comment('0待执行 1执行中 2已完成 3已取消');
$table->text('remark')->nullable();
$table->timestamps();
$table->index('service_order_id');
$table->index('technician_id');
});
// 服务评价
Schema::create('service_reviews', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('service_order_id')->nullable();
$table->unsignedBigInteger('service_execution_id')->nullable();
$table->foreignId('customer_id')->constrained();
$table->tinyInteger('score')->nullable()->comment('1-5星');
$table->text('content')->nullable();
$table->timestamp('created_at')->nullable();
$table->index('service_order_id');
});
}
public function down(): void
{
Schema::dropIfExists('service_reviews');
Schema::dropIfExists('service_executions');
Schema::dropIfExists('service_orders');
Schema::dropIfExists('service_packages');
Schema::dropIfExists('service_items');
}
};
@@ -0,0 +1,84 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// 月嫂档案
Schema::create('nannies', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('name', 50);
$table->string('phone', 20)->nullable();
$table->string('id_card', 20)->nullable();
$table->string('avatar', 255)->nullable();
$table->tinyInteger('level')->nullable()->comment('1初级 2中级 3高级 4金牌');
$table->json('skills')->nullable()->comment('技能标签');
$table->integer('experience_years')->nullable();
$table->string('health_cert', 255)->nullable()->comment('健康证');
$table->tinyInteger('cooperation_type')->nullable()->comment('1自有 2合作 3兼职');
$table->decimal('base_salary', 10, 2)->nullable();
$table->text('introduction')->nullable();
$table->tinyInteger('status')->default(1)->comment('1空闲 2服务中 3休假 4停用');
$table->timestamps();
$table->index(['store_id', 'status']);
});
// 月嫂订单
Schema::create('nanny_orders', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->foreignId('customer_id')->constrained();
$table->unsignedBigInteger('nanny_id')->nullable();
$table->string('order_no', 50)->unique();
$table->tinyInteger('service_type')->nullable()->comment('1月嫂 2育儿嫂');
$table->date('start_date')->nullable();
$table->date('end_date')->nullable();
$table->integer('days')->nullable();
$table->decimal('price', 12, 2)->nullable();
$table->tinyInteger('status')->default(0)->comment('0待匹配 1待确认 2已签约 3服务中 4已完成 5已取消');
$table->json('match_candidates')->nullable()->comment('匹配候选月嫂');
$table->text('remark')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
$table->index('store_id');
$table->index('nanny_id');
});
// 月嫂排班
Schema::create('nanny_schedules', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('nanny_id');
$table->unsignedBigInteger('nanny_order_id')->nullable();
$table->date('schedule_date');
$table->tinyInteger('type')->nullable()->comment('1服务 2休息 3培训');
$table->string('remark', 255)->nullable();
$table->timestamp('created_at')->nullable();
$table->index(['nanny_id', 'schedule_date']);
});
// 月嫂评价
Schema::create('nanny_reviews', function (Blueprint $table) {
$table->id();
$table->foreignId('nanny_order_id')->constrained()->cascadeOnDelete();
$table->foreignId('customer_id')->constrained();
$table->unsignedBigInteger('nanny_id');
$table->tinyInteger('score')->nullable()->comment('1-5');
$table->text('content')->nullable();
$table->timestamp('created_at')->nullable();
$table->index('nanny_id');
});
}
public function down(): void
{
Schema::dropIfExists('nanny_reviews');
Schema::dropIfExists('nanny_schedules');
Schema::dropIfExists('nanny_orders');
Schema::dropIfExists('nannies');
}
};
@@ -0,0 +1,106 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// 仓库
Schema::create('warehouses', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('name', 50);
$table->unsignedBigInteger('manager_id')->nullable();
$table->tinyInteger('status')->default(1);
$table->timestamps();
});
// 供应商
Schema::create('suppliers', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('name', 100);
$table->string('contact', 50)->nullable();
$table->string('phone', 20)->nullable();
$table->string('address', 255)->nullable();
$table->string('bank_info', 255)->nullable();
$table->tinyInteger('status')->default(1);
$table->timestamps();
});
// 物资(SKU
Schema::create('materials', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('name', 100);
$table->string('code', 50)->nullable()->comment('SKU编码');
$table->string('category', 50)->nullable();
$table->string('unit', 20)->nullable()->comment('单位');
$table->string('spec', 100)->nullable()->comment('规格');
$table->integer('min_stock')->default(0)->comment('安全库存');
$table->integer('shelf_life_days')->nullable()->comment('保质天数');
$table->tinyInteger('status')->default(1);
$table->timestamps();
$table->index('store_id');
$table->index('code');
});
// 库存
Schema::create('inventories', function (Blueprint $table) {
$table->id();
$table->foreignId('warehouse_id')->constrained();
$table->foreignId('material_id')->constrained();
$table->decimal('quantity', 12, 2)->default(0);
$table->string('batch_no', 50)->nullable();
$table->date('expire_date')->nullable();
$table->timestamp('updated_at')->nullable();
$table->unique(['warehouse_id', 'material_id', 'batch_no'], 'idx_wh_mat_batch');
});
// 采购单
Schema::create('purchase_orders', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->string('order_no', 50)->unique();
$table->unsignedBigInteger('supplier_id')->nullable();
$table->decimal('total_amount', 12, 2)->nullable();
$table->tinyInteger('status')->default(0)->comment('0草稿 1待审批 2已审批 3已入库 4已取消');
$table->json('items')->nullable()->comment('采购明细');
$table->unsignedBigInteger('audit_user_id')->nullable();
$table->timestamp('audit_at')->nullable();
$table->text('remark')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
});
// 出入库单
Schema::create('stock_movements', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained();
$table->foreignId('warehouse_id')->constrained();
$table->string('movement_no', 50)->unique();
$table->tinyInteger('type')->comment('1入库 2出库 3调拨 4盘点');
$table->tinyInteger('direction')->nullable()->comment('1入 2出');
$table->json('items')->nullable()->comment('物资明细');
$table->string('related_order', 50)->nullable()->comment('关联单号');
$table->tinyInteger('status')->default(0)->comment('0草稿 1已确认');
$table->unsignedBigInteger('operator_id')->nullable();
$table->text('remark')->nullable();
$table->timestamps();
$table->index(['store_id', 'type']);
});
}
public function down(): void
{
Schema::dropIfExists('stock_movements');
Schema::dropIfExists('purchase_orders');
Schema::dropIfExists('inventories');
Schema::dropIfExists('materials');
Schema::dropIfExists('suppliers');
Schema::dropIfExists('warehouses');
}
};
+71
View File
@@ -31,6 +31,19 @@ use App\Http\Controllers\Admin\Meal\DishController;
use App\Http\Controllers\Admin\Meal\MealPlanTemplateController;
use App\Http\Controllers\Admin\Meal\DailyMealPlanController;
use App\Http\Controllers\Admin\Meal\MealReviewController;
use App\Http\Controllers\Admin\Service\ServiceItemController;
use App\Http\Controllers\Admin\Service\ServicePackageController;
use App\Http\Controllers\Admin\Service\ServiceOrderController;
use App\Http\Controllers\Admin\Service\ServiceExecutionController;
use App\Http\Controllers\Admin\Nanny\NannyController;
use App\Http\Controllers\Admin\Nanny\NannyOrderController;
use App\Http\Controllers\Admin\Nanny\NannyScheduleController;
use App\Http\Controllers\Admin\Inventory\WarehouseController;
use App\Http\Controllers\Admin\Inventory\SupplierController;
use App\Http\Controllers\Admin\Inventory\MaterialController;
use App\Http\Controllers\Admin\Inventory\PurchaseOrderController;
use App\Http\Controllers\Admin\Inventory\StockMovementController;
use App\Http\Controllers\Admin\Inventory\InventoryController;
/*
|--------------------------------------------------------------------------
@@ -176,4 +189,62 @@ Route::middleware(['auth:sanctum', 'store', 'oplog'])->group(function () {
Route::get('reviews', [MealReviewController::class, 'index']);
Route::post('reviews', [MealReviewController::class, 'store']);
});
// --- 服务产康模块 ---
Route::prefix('service')->group(function () {
// 服务项目
Route::apiResource('items', ServiceItemController::class);
// 服务套餐
Route::apiResource('packages', ServicePackageController::class);
// 服务订单
Route::apiResource('orders', ServiceOrderController::class);
Route::put('orders/{serviceOrder}/pay', [ServiceOrderController::class, 'pay']);
Route::put('orders/{serviceOrder}/complete', [ServiceOrderController::class, 'complete']);
// 服务执行
Route::get('executions', [ServiceExecutionController::class, 'index']);
Route::post('executions', [ServiceExecutionController::class, 'store']);
Route::put('executions/{serviceExecution}/start', [ServiceExecutionController::class, 'start']);
Route::put('executions/{serviceExecution}/finish', [ServiceExecutionController::class, 'finish']);
});
// --- 月嫂管理模块 ---
Route::prefix('nanny')->group(function () {
// 月嫂信息
Route::apiResource('nannies', NannyController::class);
// 月嫂订单
Route::apiResource('orders', NannyOrderController::class);
// 月嫂排班
Route::get('schedules', [NannyScheduleController::class, 'index']);
Route::post('schedules', [NannyScheduleController::class, 'store']);
});
// --- 进销存模块 ---
Route::prefix('inventory')->group(function () {
// 仓库管理
Route::apiResource('warehouses', WarehouseController::class);
// 供应商管理
Route::apiResource('suppliers', SupplierController::class);
// 物料管理
Route::apiResource('materials', MaterialController::class);
// 采购管理
Route::apiResource('purchase-orders', PurchaseOrderController::class);
Route::put('purchase-orders/{purchaseOrder}/audit', [PurchaseOrderController::class, 'audit']);
// 出入库流水
Route::get('stock-movements', [StockMovementController::class, 'index']);
Route::post('stock-movements', [StockMovementController::class, 'store']);
Route::get('stock-movements/{stockMovement}', [StockMovementController::class, 'show']);
Route::put('stock-movements/{stockMovement}/confirm', [StockMovementController::class, 'confirm']);
// 库存查询
Route::get('inventories', [InventoryController::class, 'index']);
});
});