Разработка мобильного приложения для управления арендой оборудования
Аренда оборудования — инструменты, строительная техника, аудио/видео, медицинское оборудование — это история про учёт: кто взял, на сколько, в каком состоянии, когда вернул и не сломал ли. Мобильное приложение здесь решает две задачи одновременно: клиентское приложение для бронирования и операторское для управления парком.
Инвентаризация через QR/штрихкод
Каждая единица оборудования имеет уникальный идентификатор — QR-код или штрихкод, наклеенный на корпус. Оператор сканирует его для выдачи и приёмки:
// iOS: быстрый скан через AVCaptureSession
class BarcodeScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
func setupCapture() {
let session = AVCaptureSession()
guard let device = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: device) else { return }
session.addInput(input)
let output = AVCaptureMetadataOutput()
session.addOutput(output)
output.setMetadataObjectsDelegate(self, queue: .main)
output.metadataObjectTypes = [.qr, .code128, .ean13]
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = view.bounds
view.layer.addSublayer(previewLayer)
session.startRunning()
}
func metadataOutput(
_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection
) {
guard let readableCode = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
let assetId = readableCode.stringValue else { return }
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
loadEquipmentDetails(assetId: assetId)
}
}
После скана приложение показывает карточку оборудования: название, серийный номер, текущий статус (AVAILABLE / RENTED / MAINTENANCE), историю аренд.
Акт выдачи с фотофиксацией
Перед выдачей оборудования оператор фотографирует его состояние. Фото прикрепляются к договору аренды как доказательство комплектности. Это критично для разрешения споров при возврате.
// Android: CameraX для фотофиксации состояния
private fun takeHandoverPhoto() {
val imageCapture = imageCapture ?: return
val outputOptions = ImageCapture.OutputFileOptions.Builder(
createTempFile("handover_${System.currentTimeMillis()}", ".jpg")
).build()
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val photoFile = output.savedUri?.toFile() ?: return
viewModel.addHandoverPhoto(photoFile, currentRentalId)
}
override fun onError(exception: ImageCaptureException) {
showError("Ошибка съёмки: ${exception.message}")
}
}
)
}
Фотографии сжимаются до 1200px по длинной стороне перед загрузкой на сервер — оригинальный 12MP снимок на 4 МБ для этой задачи избыточен.
Бронирование с проверкой доступности
Клиентское приложение показывает календарь доступности оборудования. Пересечения брони недопустимы. Логика проверки — на сервере, но UI должен визуально блокировать занятые даты:
// iOS: DatePicker с недоступными датами
func configureDatePicker(unavailableDates: [DateInterval]) {
datePicker.minimumDate = Date()
// Для более гибкого контроля используем кастомный calendar component
// с блокировкой конкретных диапазонов
calendarView.disabledDateRanges = unavailableDates.map {
CalendarDateRange(start: $0.start, end: $0.end)
}
}
При конкурентном бронировании (два пользователя пытаются забронировать одновременно) — серверная оптимистическая блокировка: первый получает подтверждение, второй — ошибку 409 Conflict с предложением выбрать другие даты.
Расчёт стоимости
Тарификация может быть почасовой, посуточной или комбинированной. Серверный расчёт:
def calculate_rental_cost(
equipment: Equipment,
start_date: datetime,
end_date: datetime
) -> Decimal:
duration = end_date - start_date
hours = duration.total_seconds() / 3600
if hours <= 4:
return equipment.hourly_rate * Decimal(str(hours))
elif hours <= 24:
return equipment.daily_rate # посуточная ставка выгоднее
else:
days = math.ceil(hours / 24)
return equipment.daily_rate * Decimal(str(days))
Приложение показывает расчёт в реальном времени при изменении дат — через debounced API-запрос или локальный расчёт с теми же правилами.
Депозит и залог
Для дорогостоящего оборудования берётся залог. Схема: при бронировании блокируется сумма на карте (authorization hold), при возврате оборудования в целости — разблокируется (void), при ущербе — списывается (capture).
// Stripe: authorization-only payment intent
let params = STPPaymentIntentParams(clientSecret: clientSecret)
params.captureMethod = .manual // захватим позже при необходимости
STPPaymentHandler.shared().confirmPayment(params, with: self) { status, intent, error in
// intent.id сохраняем — нужен для capture или void при возврате
}
Ориентиры по срокам
Базовая версия (каталог оборудования, бронирование, QR-инвентаризация, оплата): 5–7 недель. Операторский модуль с фотофиксацией и актами — ещё 2–3 недели. Стоимость рассчитывается индивидуально.







