diff --git a/app/Admin/Services/Train/ExaminationService.php b/app/Admin/Services/Train/ExaminationService.php index 7dce4a4..e7a4d9e 100644 --- a/app/Admin/Services/Train/ExaminationService.php +++ b/app/Admin/Services/Train/ExaminationService.php @@ -8,7 +8,9 @@ use App\Admin\Services\BaseService; use Illuminate\Support\Str; use Illuminate\Support\Facades\{Validator, Storage}; use App\Enums\ExamStatus; +use App\Enums\MessageType; use App\Models\Employee; +use App\Services\MessageService; class ExaminationService extends BaseService { @@ -71,6 +73,19 @@ class ExaminationService extends BaseService $examination->update(['exam_status' => ExamStatus::Published, 'published_at' => now()]); + (new MessageService())->create( + MessageType::Exam, + '考试通知', + '您有一张待完成试卷,请尽快完成考试。', + $employees->all(), + [ + 'examination' => [ + 'id' => $examination->id, + 'name' => $examination->name, + ], + ] + ); + return true; } diff --git a/app/Admin/routes.php b/app/Admin/routes.php index 067c329..413751f 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -227,7 +227,7 @@ Route::group([ $router->get('stores', [StoreController::class, 'shareList']); $router->get('employees', [EmployeeController::class, 'shareList']); $router->get('employee-sign-logs', [SignLogController::class, 'shareList']); - $router->get('keywords/tree-list', [KeywordController::class, 'getTreeList'])->name('api.keywords.tree-list'); + $router->get('keywords/tree-list', [KeywordController::class, 'getTreeList']); $router->get('workflow/value-options', [WorkflowController::class, 'getValueOptions']); $router->post('workflow/apply', [WorkflowController::class, 'apply']); diff --git a/app/Enums/MessageType.php b/app/Enums/MessageType.php new file mode 100644 index 0000000..8685745 --- /dev/null +++ b/app/Enums/MessageType.php @@ -0,0 +1,10 @@ +whereIn('type', [MessageType::System, MessageType::Exam]); + break; + + case 'work': + $this->whereNotIn('type', [MessageType::System, MessageType::Exam]); + break; + } + } +} diff --git a/app/Http/Controllers/Api/Auth/UserController.php b/app/Http/Controllers/Api/Auth/UserController.php index 5d18383..bf1293e 100644 --- a/app/Http/Controllers/Api/Auth/UserController.php +++ b/app/Http/Controllers/Api/Auth/UserController.php @@ -2,17 +2,18 @@ namespace App\Http\Controllers\Api\Auth; +use App\Admin\Services\EmployeeService; +use App\Enums\{UserRole, BusinessStatus}; use App\Exceptions\RuntimeException; use App\Http\Controllers\Api\Controller; +use App\Http\Resources\KeywordResource; +use App\Http\Resources\StoreResource; +use App\Models\Message; use App\Models\{Employee, Store, AdminUser}; use Illuminate\Http\{Request, Response}; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; -use App\Enums\{UserRole, BusinessStatus}; -use App\Http\Resources\KeywordResource; -use App\Admin\Services\EmployeeService; -use App\Http\Resources\StoreResource; -use Illuminate\Support\Facades\DB; /** * 个人中心 @@ -23,7 +24,11 @@ class UserController extends Controller public function profile() { $user = $this->guard()->user(); - $admin = $user->adminUser; + + $unreadMessageCount = Message::ofEmployee($user) + ->whereDoesntHave('readingLogs', fn ($query) => $query->where('employee_id', $user->id)) + ->count(); + return [ 'id' => $user->id, 'name' => $user->name, @@ -31,7 +36,7 @@ class UserController extends Controller 'avatar' => $user->avatar, 'jobs' => KeywordResource::collection($user->jobs), 'store' => $user->store ? StoreResource::make($user->store) : null, - 'unread_notifications' => 0, + 'unread_notifications' => $unreadMessageCount, // 身份: user-普通员工, store-店长, admin-管理员 'role' => $user->userRole(), ]; diff --git a/app/Http/Controllers/Api/MessageController.php b/app/Http/Controllers/Api/MessageController.php new file mode 100644 index 0000000..253704e --- /dev/null +++ b/app/Http/Controllers/Api/MessageController.php @@ -0,0 +1,77 @@ +user(); + + /** @var \Illuminate\Contracts\Pagination\Paginator */ + $paginator = Message::filter($request->input(), MessageFilter::class) + ->ofEmployee($user) + ->withCount([ + 'readingLogs' => fn ($query) => $query->where('employee_id', $user->id), + ]) + ->latest('id') + ->simplePaginate($request->query('per_page', 20)); + + return collect($paginator->items())->map(function (Message $message) { + return array_merge( + MessageResource::make($message)->resolve(), + ['is_read' => $message->reading_logs_count], + ); + }); + } + + public function show($id, Request $request): MessageResource + { + /** @var \App\Models\Employee */ + $user = $request->user(); + + $message = Message::ofEmployee($user)->findOrFail($id); + + $message->readingLogs()->firstOrCreate([ + 'employee_id' => $user->id, + ]); + + return MessageResource::make($message); + } + + public function readAll(Request $request) + { + /** @var \App\Models\Employee */ + $user = $request->user(); + + $messages = Message::ofEmployee($user) + ->whereDoesntHave('readingLogs', fn ($query) => $query->where('employee_id', $user->id)) + ->get(); + + try { + $datetime = date('Y-m-d H:i:s'); + + MessageReadingLog::insert( + $messages->map(fn (Message $message) => [ + 'message_id' => $message->id, + 'employee_id' => $user->id, + 'created_at' => $datetime, + 'updated_at' => $datetime, + ])->all() + ); + } catch (Throwable $e) { + report($e); + } + + return response()->noContent(); + } +} diff --git a/app/Http/Resources/MessageResource.php b/app/Http/Resources/MessageResource.php new file mode 100644 index 0000000..7d88472 --- /dev/null +++ b/app/Http/Resources/MessageResource.php @@ -0,0 +1,21 @@ + $this->resource->id, + 'type' => $this->resource->type, + 'title' => $this->resource->title, + 'content' => $this->resource->content, + 'additional' => $this->resource->additional, + 'created_at' => $this->resource->created_at->timestamp, + ]; + } +} diff --git a/app/Models/Message.php b/app/Models/Message.php new file mode 100644 index 0000000..b33168a --- /dev/null +++ b/app/Models/Message.php @@ -0,0 +1,58 @@ + [], + ]; + + protected $casts = [ + 'type' => MessageType::class, + 'additional' => 'json', + ]; + + protected $fillable = [ + 'type', + 'title', + 'content', + 'additional', + 'employee_ids', + ]; + + public function scopeOfEmployee(Builder $query, Employee $employee) + { + $query->whereJsonLength('employee_ids', 0)->orWhereJsonContains('employee_ids', $employee->id); + } + + public function readingLogs(): HasMany + { + return $this->hasMany(MessageReadingLog::class); + } + + protected function employeeIds(): Attribute + { + return Attribute::make( + get: function (mixed $value) { + if (! is_array($ids = json_decode($value ?? '', true))) { + $ids = []; + } + + return $ids; + }, + set: fn (mixed $value) => json_encode(is_array($value) ? $value : []), + ); + } +} diff --git a/app/Models/MessageReadingLog.php b/app/Models/MessageReadingLog.php new file mode 100644 index 0000000..4a0c0f9 --- /dev/null +++ b/app/Models/MessageReadingLog.php @@ -0,0 +1,16 @@ + $employees + */ + public function create(MessageType $type, ?string $title, ?string $content, array $employees = [], array $additional = []) + { + $employeeIds = collect($employees)->map(function ($employee) { + if ($employee instanceof Employee) { + return $employee->id; + } + return $employee; + })->all(); + + $message = Message::create([ + 'type' => $type, + 'title' => $title, + 'content' => $content, + 'additional' => $additional, + 'employee_ids' => $employeeIds, + ]); + + switch ($message->type) { + // @todo 根据消息类型发送通知 + } + } +} diff --git a/config/admin.php b/config/admin.php index 64666f6..abf4eaf 100644 --- a/config/admin.php +++ b/config/admin.php @@ -57,7 +57,7 @@ return [ ], ], 'except' => [ - + ], ], diff --git a/database/migrations/2024_04_22_102837_create_messages_table.php b/database/migrations/2024_04_22_102837_create_messages_table.php new file mode 100644 index 0000000..a3cfef9 --- /dev/null +++ b/database/migrations/2024_04_22_102837_create_messages_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('type')->comment('类型'); + $table->string('title')->nullable()->comment('标题'); + $table->text('content')->nullable()->comment('内容'); + $table->text('additional')->nullable()->comment('附加数据'); + $table->json('employee_ids')->nullable()->comment('员工IDs'); + $table->timestamps(); + + $table->index('type'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('notifications'); + } +}; diff --git a/database/migrations/2024_04_22_111856_create_message_reading_logs_table.php b/database/migrations/2024_04_22_111856_create_message_reading_logs_table.php new file mode 100644 index 0000000..a87cd53 --- /dev/null +++ b/database/migrations/2024_04_22_111856_create_message_reading_logs_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('message_id'); + $table->foreignId('employee_id'); + $table->timestamps(); + + $table->unique(['message_id', 'employee_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('message_reading_logs'); + } +}; diff --git a/database/seeders/AdminPermissionSeeder.php b/database/seeders/AdminPermissionSeeder.php index 0a12062..132d366 100644 --- a/database/seeders/AdminPermissionSeeder.php +++ b/database/seeders/AdminPermissionSeeder.php @@ -15,8 +15,8 @@ class AdminPermissionSeeder extends Seeder */ public function run() { - AdminMenu::truncate(); - AdminPermission::truncate(); + // AdminMenu::truncate(); + // AdminPermission::truncate(); $data = [ /* |-------------------------------------------------------------------------- diff --git a/routes/api.php b/routes/api.php index f003a45..f57cd05 100644 --- a/routes/api.php +++ b/routes/api.php @@ -9,6 +9,7 @@ use App\Http\Controllers\Api\FeedbackController; use App\Http\Controllers\Api\FileUploadController; use App\Http\Controllers\Api\KeywordController; use App\Http\Controllers\Api\LedgerController; +use App\Http\Controllers\Api\MessageController; use App\Http\Controllers\Api\RegionController; use App\Http\Controllers\Api\ReimbursementController; use App\Http\Controllers\Api\StatisticsController; @@ -46,6 +47,11 @@ Route::group([ // 个人账户 - 门店业绩指标任务 Route::get('/account/store-performance-tasks', [TaskPerformanceController::class, 'index']); + // 消息通知 + Route::get('/message/messages', [MessageController::class, 'index']); + Route::get('/message/messages/{message}', [MessageController::class, 'show']); + Route::post('/message/read-all', [MessageController::class, 'readAll']); + // 统计数据 - 首页统计 Route::get('/statistics/dashboard', [StatisticsController::class, 'dashboard']); // 统计数据 - 门店统计