| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- <?php
- declare(strict_types=1);
- namespace app\miniapi\service;
- use app\miniapi\http\Fields;
- use app\miniapi\http\Pagination;
- use app\miniapi\exception\ApiException;
- use app\miniapi\model\MiniTaskRecord;
- use app\miniapi\model\MiniPhoto;
- use app\miniapi\service\driver\TaskActionFactory;
- use plugin\saiadmin\app\model\water\WaterDelivery;
- use plugin\saiadmin\app\model\water\WaterEnterprise;
- use plugin\saiadmin\app\model\water\WaterSku;
- use plugin\saiadmin\app\model\water\WaterDriver;
- use plugin\saiadmin\app\model\water\WaterRoute;
- use plugin\saiadmin\app\model\water\WaterDriverShift;
- use plugin\saiadmin\app\model\water\WaterDevice;
- final class DriverService
- {
- public function __construct(private readonly Access $access)
- {
- }
- public function tasks(int $driver, array $input): array
- {
- $this->access->resource('driver', $driver);
- $page = new Pagination($input);
- $status = Fields::choice($input, 'status', [
- 'all',
- 'pending',
- 'transit',
- 'completed',
- 'exception',
- ], 'all');
- $query = WaterDelivery::query()->with(['sku', 'enterprise'])->where('driver_id', $driver);
- $states = [
- 'pending' => ['assigned'],
- 'transit' => ['transit'],
- 'completed' => ['completed'],
- 'exception' => ['returning', 'returned'],
- ];
- if ($status !== 'all') {
- $query->whereIn('status', $states[$status]);
- }
- $total = (clone $query)->count();
- $items = $query->orderByDesc('scheduled_date')->orderBy('route_id')->orderBy('stop_order')->orderBy('id')
- ->offset($page->offset())->limit($page->pageSize)->get()->map(function ($delivery) {
- return WaterPresenter::delivery($delivery) + [
- 'enterprise_name' => $delivery->enterprise?->name,
- 'route_id' => $delivery->route_id,
- 'stop_order' => (int) $delivery->stop_order,
- ];
- })->all();
- return $page->result($items, $total);
- }
- public function dashboard(int $driver): array
- {
- $driverRow = $this->access->resource('driver', $driver);
- $query = WaterDelivery::query()->with(['sku', 'enterprise'])->where('driver_id', $driver);
- return [
- 'driver' => [
- 'id' => (int) $driverRow->id,
- 'name' => $driverRow->name,
- 'vehicle' => $driverRow->vehicle,
- ],
- 'shift' => WaterDriverShift::query()->where([
- 'driver_id' => $driver,
- 'work_date' => date('Y-m-d'),
- ])->value('state'),
- 'summary' => [
- 'pending' => (clone $query)->where('status', 'assigned')->count(),
- 'transit' => (clone $query)->where('status', 'transit')->count(),
- 'completed_today' => (clone $query)->where('status', 'completed')->whereBetween('completed_at', [date('Y-m-d') . ' 00:00:00', date('Y-m-d H:i:s')])->count(),
- ],
- 'tasks' => $this->tasks($driver, [
- 'status' => 'pending',
- 'page_size' => 10,
- ]),
- 'routes' => $this->routes($driver),
- 'capabilities' => [
- 'live_gps' => false,
- 'self_accept_unassigned' => false,
- ],
- ];
- }
- public function routes(int $driver): array
- {
- $this->access->resource('driver', $driver);
- return WaterRoute::query()->where('driver_id', $driver)->whereNull('settled_at')->orderBy('work_date')->orderBy('id')->limit(50)
- ->get([
- 'id',
- 'code',
- 'work_date',
- 'region',
- 'optimization',
- ])->map(function ($routeRow) {
- $route = $routeRow->toArray();
- $route['optimization'] = is_string($routeRow->optimization) ? json_decode($routeRow->optimization, true) : $routeRow->optimization;
- $route['stops'] = WaterDelivery::query()->where('route_id', $routeRow->id)->orderBy('stop_order')->orderBy('id')
- ->get([
- 'id',
- 'address',
- 'status',
- 'quantity',
- 'longitude',
- 'latitude',
- 'origin_address',
- 'origin_longitude',
- 'origin_latitude',
- 'stop_order',
- ])->toArray();
- return $route;
- })->all();
- }
- public function detail(int $driver, int $id): array
- {
- $delivery = $this->access->driverTask($driver, $id);
- $enterprise = WaterEnterprise::query()->find($delivery->enterprise_id);
- return WaterPresenter::delivery($delivery) + [
- 'enterprise_name' => $enterprise?->name,
- 'contact' => $enterprise?->contact,
- 'phone' => $enterprise ? WaterPresenter::phone($enterprise->phone) : null,
- 'contact_phone' => $enterprise && in_array($delivery->status, ['assigned', 'transit', 'returning'], true) ? $enterprise->phone : null,
- 'longitude' => $delivery->longitude,
- 'latitude' => $delivery->latitude,
- 'route_id' => $delivery->route_id,
- 'records' => MiniTaskRecord::query()->where('delivery_id', $id)->orderBy('id')->get([
- 'id',
- 'kind',
- 'data',
- 'created_at',
- ])->toArray(),
- 'photos' => MiniPhoto::query()->where('delivery_id', $id)->orderBy('id')->get(['id', 'created_at'])->toArray(),
- 'capabilities' => [
- 'checkin' => true,
- 'photos' => true,
- 'fulfillment' => true,
- 'device_binding' => false,
- ],
- ];
- }
- private function lockTask(int $driver, int $id)
- {
- $preview = $this->access->driverTask($driver, $id);
- WaterSku::query()->where('id', $preview->sku_id)->lockForUpdate()->first();
- $driverRow = WaterDriver::query()->where('id', $driver)->lockForUpdate()->first();
- $task = WaterDelivery::query()->where('id', $id)->lockForUpdate()->firstOrFail();
- if (!$driverRow || (int) $driverRow->status !== 1 || (int) $task->driver_id !== $driver) {
- throw new ApiException(40301, '任务分配或司机状态已变更,请刷新', 403);
- }
- return $task;
- }
- public function command(int $driver, int $id, string $action, array $input): array
- {
- $this->access->driverTask($driver, $id);
- $handler = TaskActionFactory::make($action);
- Fields::only($input, $handler->fields());
- return Commands::run(
- $this->access->actor,
- Fields::text($input, 'request_key', 80),
- 'driver.' . $action . ':' . $id,
- $input,
- fn () => $handler->execute($this->lockTask($driver, $id), $input, (int) $this->access->actor->userId),
- fn () => $this->lockTask($driver, $id)
- );
- }
- public function photo(int $driver, int $id, array $input): array
- {
- $this->access->driverTask($driver, $id);
- Fields::only($input, [
- 'request_key',
- 'image_base64',
- 'latitude',
- 'longitude',
- ]);
- $location = Fields::location($input);
- $encoded = $input['image_base64'] ?? null;
- if (!is_string($encoded) || strlen($encoded) > 2800000) {
- throw new ApiException(41301, '照片不能超过2MB', 413);
- }
- $bytes = base64_decode($encoded, true);
- $info = $bytes === false ? false : @getimagesizefromstring($bytes);
- if ($bytes === false || strlen($bytes) > 2097152 || !$info || !in_array($info['mime'], ['image/jpeg', 'image/png'], true)
- || $info[0] > 4096 || $info[1] > 4096 || $info[0] * $info[1] > 8000000) {
- throw new ApiException(42201, '请上传有效JPEG/PNG图片,最长边4096像素', 422);
- }
- return Commands::run($this->access->actor, Fields::text($input, 'request_key', 80), 'driver.photo:' . $id, $input, function () use ($driver, $id, $bytes, $location) {
- $task = $this->lockTask($driver, $id);
- if ($task->status !== 'transit') {
- throw new ApiException(40902, '仅配送中的任务可上传照片', 409);
- }
- if (MiniPhoto::query()->where('delivery_id', $id)->count() >= 20) {
- throw new ApiException(40902, '该任务照片已达到20张上限', 409);
- }
- $image = @imagecreatefromstring($bytes);
- if (!$image) {
- throw new ApiException(42201, '图片无法解码', 422);
- }
- $stamp = 'DS' . $id . ' ' . date('Y-m-d H:i:s') . ' GCJ02 ' . $location['latitude'] . ',' . $location['longitude'];
- $height = imagesy($image);
- $color = imagecolorallocate($image, 255, 255, 255);
- $background = imagecolorallocate($image, 0, 0, 0);
- imagefilledrectangle($image, 0, max(0, $height - 22), imagesx($image), $height, $background);
- imagestring($image, 2, 4, max(0, $height - 18), $stamp, $color);
- ob_start();
- imagejpeg($image, null, 85);
- $sanitized = ob_get_clean();
- imagedestroy($image);
- $photo = MiniPhoto::query()->create([
- 'delivery_id' => $id,
- 'user_id' => $this->access->actor->userId,
- 'mime' => 'image/jpeg',
- 'image_data' => $sanitized,
- 'created_at' => date('Y-m-d H:i:s'),
- ] + $location);
- return [
- 'id' => (int) $photo->id,
- 'delivery_id' => $id,
- 'location_verified' => false,
- 'watermark' => 'server_time_and_client_location',
- ];
- }, fn () => $this->lockTask($driver, $id));
- }
- public function deviceCheck(int $driver, int $task, string $sn): array
- {
- $delivery = $this->access->driverTask($driver, $task);
- $device = WaterDevice::query()->where('enterprise_id', $delivery->enterprise_id)->where('sn', $sn)->first();
- if (!$device) {
- throw new ApiException(40401, '设备未绑定当前企业,请联系后台登记', 404);
- }
- return [
- 'id' => (int) $device->id,
- 'sn' => $device->sn,
- 'last_seen' => $device->last_seen,
- 'online' => $device->last_seen !== null && $device->last_seen >= date('Y-m-d H:i:s', time() - 900) && trim($device->fault) === '',
- 'network_test_passed' => null,
- 'binding_enabled' => false,
- ];
- }
- }
|