2025-01-18 23:02:45 +03:00
const mongoose = require ( "mongoose" )
const router = require ( 'express' ) . Router ( )
2025-02-22 16:58:58 +03:00
const multer = require ( 'multer' )
2025-01-18 23:02:45 +03:00
const { MasterModel } = require ( './model/master' )
const { OrderModel } = require ( './model/order' )
2025-02-22 16:58:58 +03:00
const { OrderCarImgModel } = require ( './model/order.car-img' )
2025-01-18 23:02:45 +03:00
const { orderStatus } = require ( './model/const' )
2025-03-14 08:34:16 +03:00
const { getGigaToken , getSystemPrompt , getGigaChatModel } = require ( './get-token' )
2025-01-18 23:02:45 +03:00
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
2025-03-12 17:41:48 +03:00
const isValidCarColor = ( value ) => {
if ( typeof value === 'number' ) {
return value >= 0 && value <= 7
} else if ( typeof value === 'string' ) {
return /^[#a-z0-9а -я-\s,.()]+$/i . test ( value )
}
return false
}
2025-01-18 23:02:45 +03:00
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
2025-02-22 16:58:58 +03:00
const allowedMimeTypes = [ 'image/jpeg' , 'image/png' ]
2025-03-04 19:04:38 +03:00
const sizeLimitInMegaBytes = 15
2025-02-22 16:58:58 +03:00
2025-01-18 23:02:45 +03:00
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'
} ,
2025-02-22 16:58:58 +03:00
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 `
}
} ,
2025-01-18 23:02:45 +03:00
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
2025-03-03 20:08:28 +03:00
2025-01-18 23:02:45 +03:00
if ( ! mongoose . Types . ObjectId . isValid ( id ) ) {
throw new Error ( VALIDATION _MESSAGES . orderId . invalid )
}
try {
const order = await OrderModel . findById ( id )
2025-03-03 20:08:28 +03:00
2025-01-18 23:02:45 +03:00
if ( ! order ) {
throw new Error ( VALIDATION _MESSAGES . order . notFound )
}
2025-03-03 20:41:55 +03:00
const imgProps = await OrderCarImgModel . findOne ( { orderId : order . id } )
2025-03-03 20:08:28 +03:00
2025-03-03 20:41:55 +03:00
res . status ( 200 ) . send ( { success : true , body : { ... order . toObject ( ) , ... imgProps ? . toObject ( ) } } )
2025-01-18 23:02:45 +03:00
} 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 )
}
} )
2025-02-22 16:58:58 +03:00
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 )
}
}
} )
2025-03-03 18:21:32 +03:00
const { v4 : uuidv4 } = require ( "uuid" )
const axios = require ( 'axios' )
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 ) => {
2025-03-03 19:49:11 +03:00
const gigaToken = await getGigaToken ( )
2025-03-03 18:21:32 +03:00
const rqUID = uuidv4 ( )
const body = new URLSearchParams ( {
scope : "GIGACHAT_API_PERS" ,
} )
const response = await fetch ( GIGA _CHAT _OAUTH , {
method : "POST" ,
headers : {
2025-03-03 19:49:11 +03:00
Authorization : ` Basic ${ gigaToken } ` ,
2025-03-03 18:21:32 +03:00
"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 )
}
}
2025-03-20 12:30:22 +03:00
const COLORS _MAP = [ 'white' , 'black' , 'silver' , 'gray' , 'beige-brown' , 'red' , 'blue' , 'green' ]
const getColorName = ( colorKey ) => {
if ( typeof colorKey === 'number' && COLORS _MAP [ colorKey ] ) {
return COLORS _MAP [ colorKey ]
}
return colorKey
}
const analyzeImage = async ( fileId , token , imgProps ) => {
2025-03-03 18:21:32 +03:00
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 ( {
2025-03-14 08:34:16 +03:00
model : ( await getGigaChatModel ( ) ) ? ? "GigaChat-Max" ,
2025-03-03 18:21:32 +03:00
stream : false ,
update _interval : 0 ,
messages : [
{
2025-03-03 19:49:11 +03:00
role : "system" ,
2025-03-20 12:30:22 +03:00
content : ( await getSystemPrompt ( ) ) ? ? ` Ты эксперт по оценке степени загрязнения автомобилей. Твоя задача — анализировать фотографии машин и определять степень их загрязнения.
Т е б е предоставляют фотографию , где явно выделяется одна машина ( например , она расположена в центре и имеет больший размер в кадре по сравнению с остальными ) .
ВАЖНО : Твой ответ ДОЛЖЕН быть СТРОГО в формате JSON и содержать ТОЛЬКО следующие поля :
{
"value" : число от 0 до 10 ( целое или с одним знаком после запятой ) ,
"description" : "текстовое описание на русском языке"
} .
Правила :
1. Поле "value" :
- Должно быть числом от 0 до 10 ( 0 = машина абсолютно чистая , 10 = машина максимально грязная ) ИЛИ undefined ( если не удалось оценить ) ;
2. Поле "description" :
- Должно содержать 2 - 3 предложения на русском языке ;
- Обязательно указать конкретные признаки загрязнения ;
- Объяснить , почему выставлен именно такой балл .
- Должно быть связано только с автомобилем .
Н Е ДОБАВЛЯЙ никаких дополнительных полей или комментариев вне JSON структуры . Н Е ИСПОЛЬЗУЙ markdown форматирование . О Т В Е Т ДОЛЖЕН БЫТЬ ВАЛИДНЫМ JSON . Если на фотографии нельзя явно выделить одну машину , то ОЦЕНКА ДОЛЖНА ИМЕТЬ ЗНАЧЕНИЕ undefined и в описании должно быть указано , что по фотографии не удалось оценить степень загрязнения автомобиля , при этом Н Е ОПИСЫВАЙ НИЧЕГО ДРУГОГО К Р О М Е АВТОМОБИЛЯ ` ,
2025-03-03 19:49:11 +03:00
} ,
{
role : "user" ,
2025-03-20 12:30:22 +03:00
content : ` Дай оценку для приложенного файла изображения согласно структуре, ответ должен быть на русском языке. Учти, что владелец указал, что исходный цвет машины: ${ getColorName ( imgProps . color ) } ` ,
2025-03-03 18:21:32 +03:00
attachments : [ fileId ] ,
} ,
] ,
} ) ,
} )
const data = await response . json ( )
2025-03-14 08:34:16 +03:00
console . log ( data )
2025-03-03 19:49:11 +03:00
try {
return JSON . parse ( data . choices [ 0 ] . message . content )
} catch ( error ) {
console . error ( error )
return { description : data . choices [ 0 ] . message . content }
}
2025-03-03 18:21:32 +03:00
}
2025-02-22 16:58:58 +03:00
const convertFileToBase64 = ( file ) => {
const base64Image = file . buffer . toString ( 'base64' )
2025-03-03 20:13:51 +03:00
return ` data: ${ file . mimetype } ;base64, ${ base64Image } `
2025-02-22 16:58:58 +03:00
}
2025-03-03 19:49:11 +03:00
process . env [ "NODE_TLS_REJECT_UNAUTHORIZED" ] = "0"
2025-02-22 16:58:58 +03:00
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 )
}
2025-03-03 19:49:11 +03:00
try {
2025-03-03 20:41:55 +03:00
await OrderCarImgModel . deleteMany ( { orderId } )
2025-03-03 19:49:11 +03:00
const { access _token } = await getToken ( req , res )
2025-03-03 18:21:32 +03:00
2025-03-03 19:49:11 +03:00
const fileId = await uploadImage ( req . file , access _token )
2025-03-20 12:30:22 +03:00
const { value , description } = await analyzeImage ( fileId , access _token , { carColor : order . carColor } ) ? ? { }
2025-03-03 18:21:32 +03:00
2025-03-03 19:49:11 +03:00
const orderCarImg = await OrderCarImgModel . create ( {
image : convertFileToBase64 ( req . file ) ,
imageRating : value ,
imageDescription : description ,
2025-03-03 20:08:28 +03:00
orderId : order . id ,
2025-03-03 19:49:11 +03:00
created : new Date ( ) . toISOString ( ) ,
} )
2025-02-22 16:58:58 +03:00
2025-03-03 19:49:11 +03:00
res . status ( 200 ) . send ( { success : true , body : orderCarImg } )
} catch ( error ) {
console . error ( error )
}
2025-02-22 16:58:58 +03:00
} )
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 } )
}
}
2025-03-03 18:21:32 +03:00
2025-02-22 16:58:58 +03:00
throw new Error ( err . message )
} )
2025-03-03 20:08:28 +03:00
module . exports = router