feat: 第五阶段办公协同/统计报表/知识库模块

- 新增3个migration(9张表): office(6表)/report(1表)/kb(2表)
- 新增9个Model + 9个Controller
- 新增43条API路由(总计305条)
- 新增3个前端API模块 + 10个管理页面
- 迁移运行通过, vite build验证通过
This commit is contained in:
li
2026-03-13 21:43:10 +08:00
parent fb5e0daf1a
commit 058ee507fa
34 changed files with 2595 additions and 0 deletions
@@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers\Admin\Kb;
use App\Http\Controllers\Controller;
use App\Models\Kb\KbArticle;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class KbArticleController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = KbArticle::query()->with(['category', 'author']);
$query->when($request->category_id, fn($q, $v) => $q->where('category_id', $v));
$query->when($request->filled('status'), fn($q) => $q->where('status', $request->status));
$query->when($request->filled('visibility'), fn($q) => $q->where('visibility', $request->visibility));
$query->when($request->keyword, fn($q, $v) => $q->where('title', 'like', "%{$v}%"));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'nullable|string',
'category_id' => 'nullable|exists:kb_categories,id',
'visibility' => 'nullable|integer|in:1,2',
'cover_image' => 'nullable|string|max:255',
'attachments' => 'nullable|array',
]);
$validated['author_id'] = auth()->id();
$validated['status'] = 0;
$article = KbArticle::create($validated);
return $this->success($article->load(['category', 'author']));
}
public function show(KbArticle $kbArticle): JsonResponse
{
$kbArticle->increment('views');
return $this->success($kbArticle->load(['category', 'author']));
}
public function update(Request $request, KbArticle $kbArticle): JsonResponse
{
$validated = $request->validate([
'title' => 'sometimes|string|max:255',
'content' => 'nullable|string',
'category_id' => 'nullable|exists:kb_categories,id',
'visibility' => 'nullable|integer|in:1,2',
'cover_image' => 'nullable|string|max:255',
'attachments' => 'nullable|array',
]);
$kbArticle->update($validated);
return $this->success($kbArticle);
}
public function destroy(KbArticle $kbArticle): JsonResponse
{
$kbArticle->delete();
return $this->success(null);
}
public function publish(KbArticle $kbArticle): JsonResponse
{
if ($kbArticle->status >= 2) {
return $this->error('当前状态不允许发布');
}
$kbArticle->update(['status' => 2, 'published_at' => now()]);
return $this->success($kbArticle);
}
public function offline(KbArticle $kbArticle): JsonResponse
{
if ($kbArticle->status !== 2) {
return $this->error('仅已发布文章可下架');
}
$kbArticle->update(['status' => 3]);
return $this->success($kbArticle);
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Admin\Kb;
use App\Http\Controllers\Controller;
use App\Models\Kb\KbCategory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class KbCategoryController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = KbCategory::query()->with('children');
$query->when($request->filled('status'), fn($q) => $q->where('status', $request->status));
$query->where('parent_id', 0);
return $this->success($query->orderBy('sort')->orderBy('id')->get());
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'parent_id' => 'nullable|integer|min:0',
'sort' => 'nullable|integer',
'status' => 'nullable|integer|in:0,1',
]);
$validated['parent_id'] = $validated['parent_id'] ?? 0;
$category = KbCategory::create($validated);
return $this->success($category);
}
public function show(KbCategory $kbCategory): JsonResponse
{
return $this->success($kbCategory->load('children'));
}
public function update(Request $request, KbCategory $kbCategory): JsonResponse
{
$validated = $request->validate([
'name' => 'sometimes|string|max:100',
'parent_id' => 'nullable|integer|min:0',
'sort' => 'nullable|integer',
'status' => 'nullable|integer|in:0,1',
]);
$kbCategory->update($validated);
return $this->success($kbCategory);
}
public function destroy(KbCategory $kbCategory): JsonResponse
{
if ($kbCategory->children()->exists()) {
return $this->error('请先删除子分类');
}
if ($kbCategory->articles()->exists()) {
return $this->error('该分类下还有文章,无法删除');
}
$kbCategory->delete();
return $this->success(null);
}
}
@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Admin\Office;
use App\Http\Controllers\Controller;
use App\Models\Office\Announcement;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class AnnouncementController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Announcement::query()->with('author');
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
$query->when($request->has('publish_status'), fn($q) => $q->where('publish_status', $request->publish_status));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'type' => 'sometimes|integer|in:1,2,3',
'is_top' => 'sometimes|boolean',
]);
$validated['author_id'] = auth()->id();
return $this->success(Announcement::create($validated));
}
public function show(Announcement $announcement): JsonResponse
{
return $this->success($announcement->load('author'));
}
public function update(Request $request, Announcement $announcement): JsonResponse
{
$validated = $request->validate([
'title' => 'sometimes|string|max:255',
'content' => 'sometimes|string',
'type' => 'sometimes|integer|in:1,2,3',
'is_top' => 'sometimes|boolean',
]);
$announcement->update($validated);
return $this->success($announcement);
}
public function destroy(Announcement $announcement): JsonResponse
{
$announcement->delete();
return $this->success(null);
}
public function publish(Announcement $announcement): JsonResponse
{
if ($announcement->publish_status === 1) {
return $this->error('该公告已发布', 40001);
}
$announcement->update([
'publish_status' => 1,
'published_at' => now(),
]);
return $this->success($announcement);
}
}
@@ -0,0 +1,127 @@
<?php
namespace App\Http\Controllers\Admin\Office;
use App\Http\Controllers\Controller;
use App\Models\Office\Approval;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ApprovalController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Approval::query()->with(['template', 'applicant']);
$query->when($request->has('status'), fn($q) => $q->where('status', $request->status));
$query->when($request->applicant_id, fn($q, $v) => $q->where('applicant_id', $v));
return $this->paginate($query->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'template_id' => 'nullable|exists:approval_templates,id',
'form_data' => 'nullable|array',
'related_type' => 'nullable|string|max:50',
'related_id' => 'nullable|integer',
]);
$validated['applicant_id'] = auth()->id();
$validated['status'] = 0;
return $this->success(Approval::create($validated));
}
public function show(Approval $approval): JsonResponse
{
return $this->success($approval->load(['template', 'applicant', 'nodes.approver']));
}
public function update(Request $request, Approval $approval): JsonResponse
{
if ($approval->status > 1) {
return $this->error('该审批已完结,无法修改', 40001);
}
$validated = $request->validate([
'title' => 'sometimes|string|max:255',
'form_data' => 'nullable|array',
]);
$approval->update($validated);
return $this->success($approval);
}
public function destroy(Approval $approval): JsonResponse
{
if ($approval->status > 1) {
return $this->error('该审批已完结,无法删除', 40001);
}
$approval->delete();
return $this->success(null);
}
public function approve(Approval $approval): JsonResponse
{
$node = $approval->nodes()
->where('approver_id', auth()->id())
->where('action', 0)
->where('node_order', $approval->current_node)
->first();
if (!$node) {
return $this->error('无待处理的审批节点', 40001);
}
$node->update(['action' => 1, 'acted_at' => now()]);
$pendingNodes = $approval->nodes()->where('action', 0)->exists();
if (!$pendingNodes) {
$approval->update(['status' => 2]);
} else {
$approval->update([
'status' => 1,
'current_node' => $approval->current_node + 1,
]);
}
return $this->success($approval->load('nodes'));
}
public function reject(Request $request, Approval $approval): JsonResponse
{
$node = $approval->nodes()
->where('approver_id', auth()->id())
->where('action', 0)
->where('node_order', $approval->current_node)
->first();
if (!$node) {
return $this->error('无待处理的审批节点', 40001);
}
$validated = $request->validate([
'comment' => 'nullable|string|max:255',
]);
$node->update([
'action' => 2,
'comment' => $validated['comment'] ?? null,
'acted_at' => now(),
]);
$approval->update(['status' => 3]);
return $this->success($approval->load('nodes'));
}
public function withdraw(Approval $approval): JsonResponse
{
if ($approval->status > 1) {
return $this->error('该审批已完结,无法撤回', 40001);
}
$approval->update(['status' => 4]);
return $this->success($approval);
}
}
@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Admin\Office;
use App\Http\Controllers\Controller;
use App\Models\Office\ApprovalTemplate;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ApprovalTemplateController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = ApprovalTemplate::query();
$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->orderBy('sort')->latest()->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'type' => 'required|integer|in:1,2,3,4,5',
'form_fields' => 'nullable|array',
'flow_nodes' => 'nullable|array',
'status' => 'sometimes|integer|in:0,1',
'sort' => 'sometimes|integer',
]);
return $this->success(ApprovalTemplate::create($validated));
}
public function show(ApprovalTemplate $approvalTemplate): JsonResponse
{
return $this->success($approvalTemplate);
}
public function update(Request $request, ApprovalTemplate $approvalTemplate): JsonResponse
{
$validated = $request->validate([
'name' => 'sometimes|string|max:100',
'type' => 'sometimes|integer|in:1,2,3,4,5',
'form_fields' => 'nullable|array',
'flow_nodes' => 'nullable|array',
'status' => 'sometimes|integer|in:0,1',
'sort' => 'sometimes|integer',
]);
$approvalTemplate->update($validated);
return $this->success($approvalTemplate);
}
public function destroy(ApprovalTemplate $approvalTemplate): JsonResponse
{
$approvalTemplate->delete();
return $this->success(null);
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Admin\Office;
use App\Http\Controllers\Controller;
use App\Models\Office\Handover;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class HandoverController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Handover::query()->with(['handoverUser', 'receiverUser']);
$query->when($request->shift_date, fn($q, $v) => $q->where('shift_date', $v));
$query->when($request->shift_type, fn($q, $v) => $q->where('shift_type', $v));
return $this->paginate($query->latest('created_at')->paginate($request->input('per_page', 15)));
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'shift_date' => 'required|date',
'shift_type' => 'required|integer|in:1,2,3,4',
'content' => 'required|string',
'issues' => 'nullable|string',
]);
$validated['handover_user_id'] = auth()->id();
return $this->success(Handover::create($validated));
}
public function show(Handover $handover): JsonResponse
{
return $this->success($handover->load(['handoverUser', 'receiverUser']));
}
public function confirm(Handover $handover): JsonResponse
{
if ($handover->status === 1) {
return $this->error('该交接班已确认', 40001);
}
$handover->update([
'receiver_user_id' => auth()->id(),
'status' => 1,
'confirmed_at' => now(),
]);
return $this->success($handover);
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Admin\Office;
use App\Http\Controllers\Controller;
use App\Models\Office\Notification;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class NotificationController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Notification::query()->where('user_id', auth()->id());
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
$query->when($request->has('is_read'), fn($q) => $q->where('is_read', $request->boolean('is_read')));
return $this->paginate($query->latest('created_at')->paginate($request->input('per_page', 15)));
}
public function markRead(Notification $notification): JsonResponse
{
if ($notification->user_id !== auth()->id()) {
return $this->error('无权操作', 40003, 403);
}
$notification->update(['is_read' => true, 'read_at' => now()]);
return $this->success($notification);
}
public function markAllRead(): JsonResponse
{
Notification::query()
->where('user_id', auth()->id())
->where('is_read', false)
->update(['is_read' => true, 'read_at' => now()]);
return $this->success(null);
}
}
@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers\Admin\Report;
use App\Http\Controllers\Controller;
use App\Models\Crm\Contract;
use App\Models\Crm\Customer;
use App\Models\Crm\Lead;
use App\Models\Finance\FinanceRecord;
use App\Models\Room\Room;
use App\Models\Room\RoomType;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ReportController extends Controller
{
public function overview(Request $request): JsonResponse
{
$data = [
'customer_count' => Customer::count(),
'contract_amount' => Contract::where('status', 2)->sum('actual_amount'),
'room_count' => Room::count(),
'occupied_count' => Room::where('status', 3)->count(),
'today_revenue' => FinanceRecord::where('type', 1)->where('audit_status', 1)
->whereDate('record_date', today())->sum('amount'),
'month_revenue' => FinanceRecord::where('type', 1)->where('audit_status', 1)
->whereMonth('record_date', now()->month)
->whereYear('record_date', now()->year)->sum('amount'),
];
return $this->success($data);
}
public function revenue(Request $request): JsonResponse
{
$year = $request->input('year', now()->year);
$records = FinanceRecord::where('audit_status', 1)
->whereYear('record_date', $year)
->selectRaw('MONTH(record_date) as month, type, SUM(amount) as total')
->groupBy('month', 'type')
->get();
return $this->success($records);
}
public function occupancy(Request $request): JsonResponse
{
$data = RoomType::withCount([
'rooms',
'rooms as occupied_count' => fn($q) => $q->where('status', 3),
])->get();
return $this->success($data);
}
public function crmConversion(Request $request): JsonResponse
{
$data = [
'total_leads' => Lead::count(),
'following' => Lead::where('status', 2)->count(),
'contracted' => Lead::where('status', 5)->count(),
'by_channel' => Lead::selectRaw('channel_id, COUNT(*) as total, SUM(CASE WHEN status=5 THEN 1 ELSE 0 END) as converted')
->groupBy('channel_id')
->with('channel:id,name')
->get(),
];
return $this->success($data);
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Admin\Report;
use App\Http\Controllers\Controller;
use App\Models\Report\ReportExport;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ReportExportController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = ReportExport::query()->with('operator');
$query->when($request->type, fn($q, $v) => $q->where('type', $v));
$query->when($request->filled('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:255',
'type' => 'required|string|max:50',
'params' => 'nullable|array',
]);
$validated['operator_id'] = auth()->id();
$validated['status'] = 0;
$export = ReportExport::create($validated);
return $this->success($export);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Models\Kb;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class KbArticle extends Model
{
use BelongsToStore;
protected $fillable = [
'store_id', 'category_id', 'title', 'content', 'author_id',
'status', 'visibility', 'cover_image', 'attachments',
'views', 'likes', 'favorites', 'published_at',
];
protected function casts(): array
{
return [
'status' => 'integer',
'visibility' => 'integer',
'views' => 'integer',
'likes' => 'integer',
'favorites' => 'integer',
'attachments' => 'array',
'published_at' => 'datetime',
];
}
public function category(): BelongsTo
{
return $this->belongsTo(KbCategory::class, 'category_id');
}
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Models\Kb;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class KbCategory extends Model
{
use BelongsToStore;
protected $fillable = [
'store_id', 'parent_id', 'name', 'sort', 'status',
];
protected function casts(): array
{
return [
'sort' => 'integer',
'status' => 'integer',
];
}
public function children(): HasMany
{
return $this->hasMany(self::class, 'parent_id');
}
public function parent(): BelongsTo
{
return $this->belongsTo(self::class, 'parent_id');
}
public function articles(): HasMany
{
return $this->hasMany(KbArticle::class, 'category_id');
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Models\Office;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Announcement extends Model
{
use BelongsToStore;
protected $fillable = [
'store_id', 'title', 'content', 'type', 'is_top',
'publish_status', 'published_at', 'author_id',
];
protected function casts(): array
{
return [
'type' => 'integer',
'publish_status' => 'integer',
'is_top' => 'boolean',
'published_at' => 'datetime',
];
}
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Models\Office;
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 Approval extends Model
{
use BelongsToStore;
protected $fillable = [
'store_id', 'template_id', 'title', 'applicant_id', 'form_data',
'status', 'current_node', 'related_type', 'related_id',
];
protected function casts(): array
{
return [
'status' => 'integer',
'current_node' => 'integer',
'form_data' => 'array',
];
}
public function template(): BelongsTo
{
return $this->belongsTo(ApprovalTemplate::class, 'template_id');
}
public function applicant(): BelongsTo
{
return $this->belongsTo(User::class, 'applicant_id');
}
public function nodes(): HasMany
{
return $this->hasMany(ApprovalNode::class);
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Models\Office;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ApprovalNode extends Model
{
const UPDATED_AT = null;
protected $fillable = [
'approval_id', 'node_order', 'approver_id', 'action', 'comment', 'acted_at',
];
protected function casts(): array
{
return [
'node_order' => 'integer',
'action' => 'integer',
'acted_at' => 'datetime',
];
}
public function approval(): BelongsTo
{
return $this->belongsTo(Approval::class);
}
public function approver(): BelongsTo
{
return $this->belongsTo(User::class, 'approver_id');
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Models\Office;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
class ApprovalTemplate extends Model
{
use BelongsToStore;
protected $fillable = [
'store_id', 'name', 'type', 'form_fields', 'flow_nodes', 'status', 'sort',
];
protected function casts(): array
{
return [
'type' => 'integer',
'status' => 'integer',
'sort' => 'integer',
'form_fields' => 'array',
'flow_nodes' => 'array',
];
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Models\Office;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Handover extends Model
{
use BelongsToStore;
const UPDATED_AT = null;
protected $fillable = [
'store_id', 'shift_date', 'shift_type', 'handover_user_id',
'receiver_user_id', 'content', 'issues', 'status', 'confirmed_at',
];
protected function casts(): array
{
return [
'shift_type' => 'integer',
'status' => 'integer',
'shift_date' => 'date',
'confirmed_at' => 'datetime',
];
}
public function handoverUser(): BelongsTo
{
return $this->belongsTo(User::class, 'handover_user_id');
}
public function receiverUser(): BelongsTo
{
return $this->belongsTo(User::class, 'receiver_user_id');
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Models\Office;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Notification extends Model
{
use BelongsToStore;
const UPDATED_AT = null;
protected $table = 'notifications';
protected $fillable = [
'store_id', 'user_id', 'title', 'content', 'type',
'related_type', 'related_id', 'is_read', 'read_at',
];
protected function casts(): array
{
return [
'type' => 'integer',
'is_read' => 'boolean',
'read_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Models\Report;
use App\Models\User;
use App\Traits\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ReportExport extends Model
{
use BelongsToStore;
const UPDATED_AT = null;
protected $fillable = [
'store_id', 'name', 'type', 'params', 'file_path', 'status', 'operator_id',
];
protected function casts(): array
{
return [
'params' => 'array',
'status' => 'integer',
];
}
public function operator(): BelongsTo
{
return $this->belongsTo(User::class, 'operator_id');
}
}
@@ -0,0 +1,105 @@
<?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('approval_templates', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('store_id')->index();
$table->string('name', 100);
$table->tinyInteger('type')->comment('1=请假 2=采购 3=合同 4=退款 5=换房');
$table->json('form_fields')->nullable();
$table->json('flow_nodes')->nullable();
$table->tinyInteger('status')->default(1);
$table->integer('sort')->default(0);
$table->timestamps();
});
// 审批记录
Schema::create('approvals', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('store_id')->index();
$table->unsignedBigInteger('template_id')->nullable();
$table->string('title');
$table->unsignedBigInteger('applicant_id')->index();
$table->json('form_data')->nullable();
$table->tinyInteger('status')->default(0)->comment('0=待审批 1=审批中 2=已通过 3=已驳回 4=已撤回');
$table->integer('current_node')->default(0);
$table->string('related_type', 50)->nullable();
$table->unsignedBigInteger('related_id')->nullable();
$table->timestamps();
});
// 审批节点
Schema::create('approval_nodes', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('approval_id')->index();
$table->integer('node_order')->default(0);
$table->unsignedBigInteger('approver_id');
$table->tinyInteger('action')->default(0)->comment('0=待处理 1=通过 2=驳回');
$table->string('comment')->nullable();
$table->timestamp('acted_at')->nullable();
$table->timestamp('created_at')->nullable();
});
// 公告
Schema::create('announcements', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('store_id')->index();
$table->string('title');
$table->text('content');
$table->tinyInteger('type')->default(1)->comment('1=通知 2=公告 3=制度');
$table->boolean('is_top')->default(false);
$table->tinyInteger('publish_status')->default(0)->comment('0=草稿 1=已发布');
$table->timestamp('published_at')->nullable();
$table->unsignedBigInteger('author_id')->nullable();
$table->timestamps();
});
// 交接班
Schema::create('handovers', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('store_id')->index();
$table->date('shift_date');
$table->tinyInteger('shift_type')->comment('1=早班 2=中班 3=晚班 4=夜班');
$table->unsignedBigInteger('handover_user_id');
$table->unsignedBigInteger('receiver_user_id')->nullable();
$table->text('content');
$table->text('issues')->nullable();
$table->tinyInteger('status')->default(0)->comment('0=待确认 1=已确认');
$table->timestamp('confirmed_at')->nullable();
$table->timestamp('created_at')->nullable();
});
// 消息通知
Schema::create('notifications', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('store_id')->index();
$table->unsignedBigInteger('user_id')->index();
$table->string('title');
$table->string('content')->nullable();
$table->tinyInteger('type')->default(1)->comment('1=系统 2=审批 3=待办 4=提醒');
$table->string('related_type', 50)->nullable();
$table->unsignedBigInteger('related_id')->nullable();
$table->boolean('is_read')->default(false);
$table->timestamp('read_at')->nullable();
$table->timestamp('created_at')->nullable();
});
}
public function down(): void
{
Schema::dropIfExists('notifications');
Schema::dropIfExists('handovers');
Schema::dropIfExists('announcements');
Schema::dropIfExists('approval_nodes');
Schema::dropIfExists('approvals');
Schema::dropIfExists('approval_templates');
}
};
@@ -0,0 +1,28 @@
<?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('report_exports', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('store_id')->index();
$table->string('name');
$table->string('type', 50)->comment('report type key');
$table->json('params')->nullable();
$table->string('file_path')->nullable();
$table->tinyInteger('status')->default(0)->comment('0=生成中 1=已完成 2=失败');
$table->unsignedBigInteger('operator_id')->nullable();
$table->timestamp('created_at')->nullable();
});
}
public function down(): void
{
Schema::dropIfExists('report_exports');
}
};
@@ -0,0 +1,46 @@
<?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('kb_categories', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('store_id')->index();
$table->unsignedBigInteger('parent_id')->default(0)->index();
$table->string('name', 100);
$table->integer('sort')->default(0);
$table->tinyInteger('status')->default(1);
$table->timestamps();
});
Schema::create('kb_articles', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('store_id')->index();
$table->unsignedBigInteger('category_id')->nullable()->index();
$table->string('title');
$table->longText('content')->nullable();
$table->unsignedBigInteger('author_id')->nullable();
$table->tinyInteger('status')->default(0)->comment('0=草稿 1=待审核 2=已发布 3=已下架');
$table->tinyInteger('visibility')->default(1)->comment('1=内部 2=公开');
$table->string('cover_image')->nullable();
$table->json('attachments')->nullable();
$table->unsignedInteger('views')->default(0);
$table->unsignedInteger('likes')->default(0);
$table->unsignedInteger('favorites')->default(0);
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->fullText(['title', 'content']);
});
}
public function down(): void
{
Schema::dropIfExists('kb_articles');
Schema::dropIfExists('kb_categories');
}
};
+60
View File
@@ -55,6 +55,15 @@ use App\Http\Controllers\Admin\Hr\ScheduleController;
use App\Http\Controllers\Admin\Hr\AttendanceRecordController;
use App\Http\Controllers\Admin\Hr\LeaveRequestController;
use App\Http\Controllers\Admin\Hr\SalaryRecordController;
use App\Http\Controllers\Admin\Office\ApprovalTemplateController;
use App\Http\Controllers\Admin\Office\ApprovalController;
use App\Http\Controllers\Admin\Office\AnnouncementController;
use App\Http\Controllers\Admin\Office\HandoverController;
use App\Http\Controllers\Admin\Office\NotificationController;
use App\Http\Controllers\Admin\Report\ReportController;
use App\Http\Controllers\Admin\Report\ReportExportController;
use App\Http\Controllers\Admin\Kb\KbCategoryController;
use App\Http\Controllers\Admin\Kb\KbArticleController;
/*
|--------------------------------------------------------------------------
@@ -308,4 +317,55 @@ Route::middleware(['auth:sanctum', 'store', 'oplog'])->group(function () {
Route::put('salaries/{salaryRecord}/confirm', [SalaryRecordController::class, 'confirm']);
Route::put('salaries/{salaryRecord}/pay', [SalaryRecordController::class, 'pay']);
});
// --- 办公协同模块 ---
Route::prefix('office')->group(function () {
// 审批模板
Route::apiResource('approval-templates', ApprovalTemplateController::class);
// 审批管理
Route::apiResource('approvals', ApprovalController::class)->except(['update']);
Route::put('approvals/{approval}/approve', [ApprovalController::class, 'approve']);
Route::put('approvals/{approval}/reject', [ApprovalController::class, 'reject']);
Route::put('approvals/{approval}/withdraw', [ApprovalController::class, 'withdraw']);
// 公告管理
Route::apiResource('announcements', AnnouncementController::class);
Route::put('announcements/{announcement}/publish', [AnnouncementController::class, 'publish']);
// 交接班
Route::get('handovers', [HandoverController::class, 'index']);
Route::post('handovers', [HandoverController::class, 'store']);
Route::get('handovers/{handover}', [HandoverController::class, 'show']);
Route::put('handovers/{handover}/confirm', [HandoverController::class, 'confirm']);
// 消息通知
Route::get('notifications', [NotificationController::class, 'index']);
Route::put('notifications/{notification}/read', [NotificationController::class, 'markRead']);
Route::put('notifications/read-all', [NotificationController::class, 'markAllRead']);
});
// --- 统计报表模块 ---
Route::prefix('report')->group(function () {
// 聚合报表
Route::get('overview', [ReportController::class, 'overview']);
Route::get('revenue', [ReportController::class, 'revenue']);
Route::get('occupancy', [ReportController::class, 'occupancy']);
Route::get('crm-conversion', [ReportController::class, 'crmConversion']);
// 导出记录
Route::get('exports', [ReportExportController::class, 'index']);
Route::post('exports', [ReportExportController::class, 'store']);
});
// --- 知识库模块 ---
Route::prefix('kb')->group(function () {
// 知识分类
Route::apiResource('categories', KbCategoryController::class);
// 知识文章
Route::apiResource('articles', KbArticleController::class);
Route::put('articles/{kbArticle}/publish', [KbArticleController::class, 'publish']);
Route::put('articles/{kbArticle}/offline', [KbArticleController::class, 'offline']);
});
});