multy-stub/server/routers/dry-wash/order.js
2025-03-03 19:49:11 +03:00

442 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const mongoose = require("mongoose")
const router = require('express').Router()
const multer = require('multer')
const { MasterModel } = require('./model/master')
const { OrderModel } = require('./model/order')
const { OrderCarImgModel } = require('./model/order.car-img')
const { orderStatus } = require('./model/const')
const { getGigaToken } = require('./get-token')
const isValidPhoneNumber = (value) => /^(\+)?\d{9,15}/.test(value)
const isValidCarNumber = (value) => /^[авекмнорстух][0-9]{3}[авекмнорстух]{2}[0-9]{2,3}$/i.test(value)
const isValidCarBodyType = (value) => typeof value === 'number' && value > 0 && value < 100
const isValidCarColor = (value) => value.length < 50 && /^[#a-z0-9а-я-\s,.()]+$/i.test(value)
const isValidISODate = (value) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,3})?Z$/.test(value)
const latitudeRe = /^(-?[1-8]?\d(?:\.\d{1,18})?|90(?:\.0{1,18})?)$/
const longitudeRe = /^(-?(?:1[0-7]|[1-9])?\d(?:\.\d{1,18})?|180(?:\.0{1,18})?)$/
const addressRe = /^[а-я0-9\s,.'-/()]*$/i
const isValidLocation = (value) => {
if (value.length > 200) {
return false
}
const [coordinates, address] = value.split(' ')
const [latitude, longitude] = coordinates.split(',')
return latitudeRe.test(latitude) && longitudeRe.test(longitude) && addressRe.test(address)
}
const isValidOrderStatus = (value) => Object.values(orderStatus).includes(value)
const isValidOrderNotes = (value) => value.length < 500
const allowedMimeTypes = ['image/jpeg', 'image/png']
const sizeLimitInMegaBytes = 5
const VALIDATION_MESSAGES = {
order: {
notFound: 'Order not found'
},
orderId: {
invalid: 'Valid order ID is required',
},
orderStatus: {
invalid: 'Invalid order status'
},
orderNotes: {
invalid: 'Invalid order notes'
},
master: {
notFound: 'Master not found'
},
masterId: {
invalid: 'Invalid master ID',
},
phoneNumber: {
required: 'Phone number is required',
invalid: 'Invalid phone number'
},
carNumber: {
required: 'Car number is required',
invalid: 'Invalid car number'
},
carBody: {
required: 'Car body type is required',
invalid: 'Invalid car body type'
},
carColor: {
invalid: 'Invalid car color'
},
carImg: {
required: 'Car image file is required',
invalid: {
type: `Invalid car image file type. Allowed types: ${allowedMimeTypes}`,
size: `Invalid car image file size. Limit is ${sizeLimitInMegaBytes}MB`
}
},
washingBegin: {
required: 'Begin time of washing is required',
invalid: 'Invalid begin time of washing'
},
washingEnd: {
required: 'End time of washing is required',
invalid: 'Invalid end time of washing'
},
washingLocation: {
required: 'Location of washing is required',
invalid: 'Invalid location of washing'
},
}
router.post('/create', async (req, res, next) => {
const bodyErrors = []
const { customer } = req.body
if (!customer.phone) {
bodyErrors.push(VALIDATION_MESSAGES.phoneNumber.required)
} else if (!isValidPhoneNumber(customer.phone)) {
bodyErrors.push(VALIDATION_MESSAGES.phoneNumber.invalid)
}
const { car } = req.body
if (!car.number) {
bodyErrors.push(VALIDATION_MESSAGES.carNumber.required)
} else if (!isValidCarNumber(car.number)) {
bodyErrors.push(VALIDATION_MESSAGES.carNumber.invalid)
}
if (!car.body) {
bodyErrors.push(VALIDATION_MESSAGES.carBody.required)
} else if (!isValidCarBodyType(car.body)) {
bodyErrors.push(VALIDATION_MESSAGES.carBody.invalid)
}
if (!isValidCarColor(car.color)) {
bodyErrors.push(VALIDATION_MESSAGES.carColor.invalid)
}
const { washing } = req.body
if (!washing.begin) {
bodyErrors.push(VALIDATION_MESSAGES.washingBegin.required)
} else if (!isValidISODate(washing.begin)) {
bodyErrors.push(VALIDATION_MESSAGES.washingBegin.invalid)
}
if (!washing.end) {
bodyErrors.push(VALIDATION_MESSAGES.washingEnd.required)
} else if (!isValidISODate(washing.end)) {
bodyErrors.push(VALIDATION_MESSAGES.washingEnd.invalid)
}
if (!washing.location) {
bodyErrors.push(VALIDATION_MESSAGES.washingLocation.required)
} else if (!isValidLocation(washing.location)) {
bodyErrors.push(VALIDATION_MESSAGES.washingLocation.invalid)
}
if (bodyErrors.length > 0) {
throw new Error(bodyErrors.join(', '))
}
try {
const order = await OrderModel.create({
phone: customer.phone,
carNumber: car.number,
carBody: car.body,
carColor: car.color,
startWashTime: washing.begin,
endWashTime: washing.end,
location: washing.location,
status: orderStatus.PROGRESS,
notes: '',
created: new Date().toISOString(),
})
res.status(200).send({ success: true, body: order })
} catch (error) {
next(error)
}
})
router.get('/:id', async (req, res, next) => {
const { id } = req.params
if (!mongoose.Types.ObjectId.isValid(id)) {
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
}
try {
const order = await OrderModel.findById(id)
if (!order) {
throw new Error(VALIDATION_MESSAGES.order.notFound)
}
res.status(200).send({ success: true, body: order })
} catch (error) {
next(error)
}
})
router.patch('/:id', async (req, res, next) => {
const { id } = req.params
if (!mongoose.Types.ObjectId.isValid(id)) {
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
}
const bodyErrors = []
const { status } = req.body
if (status) {
if (!isValidOrderStatus(status)) {
bodyErrors.push(VALIDATION_MESSAGES.orderStatus.invalid)
}
}
const { master: masterId } = req.body
if (masterId) {
if (!mongoose.Types.ObjectId.isValid(masterId)) {
bodyErrors.push(VALIDATION_MESSAGES.masterId.invalid)
} else {
try {
const master = await MasterModel.findById(masterId)
if (!master) {
bodyErrors.push(VALIDATION_MESSAGES.master.notFound)
}
} catch (error) {
next(error)
}
}
}
const { notes } = req.body
if (notes) {
if (!isValidOrderNotes(notes)) {
bodyErrors.push(VALIDATION_MESSAGES.orderNotes.invalid)
}
}
if (bodyErrors.length > 0) {
throw new Error(bodyErrors.join(', '))
}
try {
const updateData = {}
if (status) {
updateData.status = status
}
if (masterId) {
updateData.master = masterId
}
if (notes) {
updateData.notes = notes
}
updateData.updated = new Date().toISOString()
const order = await OrderModel.findByIdAndUpdate(
id,
updateData,
{ new: true }
)
if (!order) {
throw new Error(VALIDATION_MESSAGES.order.notFound)
}
res.status(200).send({ success: true, body: order })
} catch (error) {
next(error)
}
})
router.delete('/:id', async (req, res, next) => {
const { id } = req.params
if (!mongoose.Types.ObjectId.isValid(id)) {
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
}
try {
const order = await OrderModel.findByIdAndDelete(id, {
new: true,
})
if (!order) {
throw new Error(VALIDATION_MESSAGES.order.notFound)
}
res.status(200).send({ success: true, body: order })
} catch (error) {
next(error)
}
})
const storage = multer.memoryStorage()
const upload = multer({
storage: storage,
limits: { fileSize: sizeLimitInMegaBytes * 1024 * 1024 },
fileFilter: (req, file, cb) => {
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true)
} else {
cb(new Error(VALIDATION_MESSAGES.carImg.invalid.type), false)
}
}
})
const { v4: uuidv4 } = require("uuid")
const axios = require('axios')
const GIGA_CHAT_ACCESS_TOKEN = 'MTQwMmNmZjgtZjA5OC00OGMxLWI0OTUtNWU3ZTU4YzMzZjdjOmU5OGFiYmNiLThmMDItNGVmOC1hNjhhLTA4Y2QxYjVmOGRmMA=='
const GIGA_CHAT_OAUTH = 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth'
const GIGA_CHAT_API = 'https://gigachat.devices.sberbank.ru/api/v1'
const getToken = async (req, res) => {
const gigaToken = await getGigaToken()
const rqUID = uuidv4()
const body = new URLSearchParams({
scope: "GIGACHAT_API_PERS",
})
const response = await fetch(GIGA_CHAT_OAUTH, {
method: "POST",
headers: {
Authorization: `Basic ${gigaToken}`,
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
RqUID: rqUID,
},
body,
})
if (!response.ok) {
const errorData = await response.json()
console.error("Ошибка запроса: ", errorData)
return res.status(response.status).json(errorData)
}
return await response.json()
}
const uploadImage = async (file, accessToken) => {
const formData = new FormData()
const blob = new Blob([file.buffer], { type: file.mimetype })
formData.append('file', blob, file.originalname)
formData.append('purpose', 'general')
const config = {
maxBodyLength: Infinity,
headers: {
'Content-Type': 'multipart/form-data',
'Accept': 'application/json',
'Authorization': `Bearer ${accessToken}`
}
}
try {
const response = await axios.post(`${GIGA_CHAT_API}/files`, formData, config)
return response.data.id
} catch (error) {
console.error(error)
}
}
const analyzeImage = async (fileId, token) => {
const response = await fetch(`${GIGA_CHAT_API}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
model: "GigaChat-Max",
stream: false,
update_interval: 0,
messages: [
{
role: "system",
content:
`Ты эксперт по оценке степени загрязнения автомобилей. Твоя задача — анализировать фотографии машин и определять степень их загрязнения.
ВАЖНО: Твой ответ ДОЛЖЕН быть СТРОГО в формате JSON и содержать ТОЛЬКО следующие поля:
{
"value": число от 0 до 10 (целое или с одним знаком после запятой),
"description": "текстовое описание на русском языке"
}
Правила:
1. Поле "value":
- Должно быть числом от 0 до 10
- 0 = машина абсолютно чистая
- 10 = машина максимально грязная
2. Поле "description":
- Должно содержать 2-3 предложения на русском языке
- Обязательно указать конкретные признаки загрязнения
- Объяснить почему выставлен именно такой балл
НЕ ДОБАВЛЯЙ никаких дополнительных полей или комментариев вне JSON структуры.
НЕ ИСПОЛЬЗУЙ markdown форматирование.
ОТВЕТ ДОЛЖЕН БЫТЬ ВАЛИДНЫМ JSON.`,
},
{
role: "user",
content: 'Дай оценку для приложенного файла изображения согласно структуре, ответ должен быть на русском языке',
attachments: [fileId],
},
],
}),
})
const data = await response.json()
try {
return JSON.parse(data.choices[0].message.content)
} catch (error) {
console.error(error)
return { description: data.choices[0].message.content }
}
}
const convertFileToBase64 = (file) => {
const base64Image = file.buffer.toString('base64')
return base64Image
}
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"
router.post('/:id/upload-car-img', upload.single('file'), async (req, res) => {
const { id: orderId } = req.params
if (!mongoose.Types.ObjectId.isValid(orderId)) {
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
}
const order = await OrderModel.findById(orderId)
if (!order) {
throw new Error(VALIDATION_MESSAGES.order.notFound)
}
if (!req.file) {
throw new Error(VALIDATION_MESSAGES.carImg.required)
}
try {
const { access_token } = await getToken(req, res)
const fileId = await uploadImage(req.file, access_token)
const { value, description } = await analyzeImage(fileId, access_token) ?? {}
const orderCarImg = await OrderCarImgModel.create({
image: convertFileToBase64(req.file),
imageRating: value,
imageDescription: description,
orderId,
created: new Date().toISOString(),
})
res.status(200).send({ success: true, body: orderCarImg })
} catch (error) {
console.error(error)
}
})
router.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
switch (err.message) {
case 'File too large':
return res.status(400).json({ success: false, error: VALIDATION_MESSAGES.carImg.invalid.size })
default:
return res.status(400).json({ success: false, error: err.message })
}
}
throw new Error(err.message)
})
module.exports = router