bindDevice($pushToken); } $device = AppPushDevice::createInstance(array_merge($attributes, [ 'userid' => $userid, 'channel' => $channel, 'device_hash' => $hash, ])); // 唯一键原子 upsert,设备切换帐号或并发上报时保留一条绑定。 AppPushDevice::upsert([$device->getAttributes()], ['channel', 'device_id'], [ 'userid', 'vendor', 'platform', 'device_hash', 'device', 'version', 'ua', 'is_notified', 'updated_at', ]); } /** * 退出时 token 可以已失效,但必须与绑定时的 token 相同。 */ public static function remove(string $deviceId, string $token): void { if ($token === '') { return; } // 与 UserDevice::check()/forget() 共用 TOKEN MD5 算法,不依赖 token 解码。 self::forgetHash(md5($token), $deviceId); } /** * 按登录 token 哈希清理推送设备(退出登录、踢下线等),系统配置的设备同时解除租户绑定 */ public static function forgetHash(string $hash, ?string $deviceId = null): void { $query = AppPushDevice::whereIn('channel', [AppPush::CHANNEL_SYSTEM, AppPush::CHANNEL_CUSTOM])->whereDeviceHash($hash) ->when($deviceId !== null, fn ($q) => $q->whereDeviceId($deviceId)); $systemIds = (clone $query)->whereChannel(AppPush::CHANNEL_SYSTEM)->pluck('device_id')->all(); $query->delete(); foreach ($systemIds as $id) { app(DooPushClient::class)->unbindDevice($id); } } }