main
panliang 2024-04-22 16:53:55 +08:00
commit d76f8e57f3
15 changed files with 343 additions and 11 deletions

View File

@ -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;
}

View File

@ -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']);

View File

@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum MessageType: string
{
case System = 'system'; // 系统消息
case Exam = 'exam'; // 考试通知
case Approval = 'approval'; // 审批通知
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Filters;
use App\Enums\MessageType;
use EloquentFilter\ModelFilter;
class MessageFilter extends ModelFilter
{
public function filter($filter)
{
switch ($filter) {
case 'system':
$this->whereIn('type', [MessageType::System, MessageType::Exam]);
break;
case 'work':
$this->whereNotIn('type', [MessageType::System, MessageType::Exam]);
break;
}
}
}

View File

@ -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(),
];

View File

@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers\Api;
use App\Filters\MessageFilter;
use App\Http\Controllers\Api\Controller;
use App\Http\Resources\MessageResource;
use App\Models\Message;
use App\Models\MessageReadingLog;
use Illuminate\Http\Request;
use Throwable;
class MessageController extends Controller
{
public function index(Request $request)
{
/** @var \App\Models\Employee */
$user = $request->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();
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class MessageResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $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,
];
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Models;
use App\Enums\MessageType;
use App\Traits\HasDateTimeFormatter;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Message extends Model
{
use Filterable, HasDateTimeFormatter, HasFactory;
protected $attributes = [
'employee_ids' => [],
];
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 : []),
);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class MessageReadingLog extends Model
{
use HasFactory;
protected $fillable = [
'message_id',
'employee_id',
];
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Services;
use App\Enums\MessageType;
use App\Models\Employee;
use App\Models\Message;
class MessageService
{
/**
* 创建消息通知
*
* @param array<int, \App\Models\Employee|int> $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 根据消息类型发送通知
}
}
}

View File

@ -57,7 +57,7 @@ return [
],
],
'except' => [
],
],

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('messages', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('message_reading_logs', function (Blueprint $table) {
$table->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');
}
};

View File

@ -15,8 +15,8 @@ class AdminPermissionSeeder extends Seeder
*/
public function run()
{
AdminMenu::truncate();
AdminPermission::truncate();
// AdminMenu::truncate();
// AdminPermission::truncate();
$data = [
/*
|--------------------------------------------------------------------------

View File

@ -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']);
// 统计数据 - 门店统计