ImportService.coffee 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. # app/services/ImportService.coffee
  2. { Product, Category } = require 'app/types/data'
  3. ProductService = require 'app/services/ProductService'
  4. CategoryService = require 'app/services/CategoryService'
  5. class ImportService
  6. constructor: ->
  7. @batchSize = 50
  8. @maxImages = 5
  9. importCSVData: (csvData, fieldMapping, domain, onProgress) ->
  10. log '🚀 Начало импорта CSV данных'
  11. try
  12. # Трансформация данных
  13. products = csvData.map (row, index) =>
  14. @transformRowToProduct(row, fieldMapping, domain, index)
  15. # Фильтрация валидных товаров
  16. validProducts = products.filter (product) =>
  17. product.name and product.sku and product.price > 0
  18. log "✅ Валидных товаров: " + validProducts.length + " из " + products.length
  19. # Пакетная обработка
  20. return await @processProductsInBatches(validProducts, domain, onProgress)
  21. catch error
  22. log '❌ Ошибка импорта CSV данных:', error
  23. throw error
  24. transformRowToProduct: (row, fieldMapping, domain, index) ->
  25. product = new Product()
  26. # Базовые поля
  27. skuValue = row[@getFieldByMapping(row, fieldMapping, 'sku')] or "temp_" + index
  28. product._id = "product:" + skuValue
  29. product.name = @getFieldByMapping(row, fieldMapping, 'name') || ''
  30. product.sku = skuValue
  31. product.price = @parsePrice(@getFieldByMapping(row, fieldMapping, 'price'))
  32. product.oldPrice = @parsePrice(@getFieldByMapping(row, fieldMapping, 'oldPrice'))
  33. product.brand = @getFieldByMapping(row, fieldMapping, 'brand') || ''
  34. product.category = @getFieldByMapping(row, fieldMapping, 'category') || ''
  35. product.description = @getFieldByMapping(row, fieldMapping, 'description') || ''
  36. product.domains = [domain]
  37. # Обработка изображений
  38. product.images = @processProductImages(row, product._id)
  39. # Rich-контент
  40. richContentField = @getFieldByMapping(row, fieldMapping, 'richContent')
  41. if richContentField
  42. try
  43. product.richContent = @jsonToMarkdown(JSON.parse(richContentField))
  44. catch error
  45. log '⚠️ Ошибка парсинга rich-контента:', error
  46. # Атрибуты
  47. product.attributes = @extractProductAttributes(row, fieldMapping)
  48. return product
  49. getFieldByMapping: (row, fieldMapping, targetField) ->
  50. for csvField, mappedField of fieldMapping
  51. if mappedField == targetField and row[csvField]
  52. return row[csvField]
  53. return ''
  54. parsePrice: (priceStr) ->
  55. return null unless priceStr
  56. price = parseFloat(priceStr.toString().replace(',', '.').replace(/\s/g, ''))
  57. return if isNaN(price) then null else price
  58. processProductImages: (row, docId) ->
  59. images = []
  60. # Главное изображение
  61. mainImage = @getFieldByMapping(row, @fieldMapping, 'mainImage')
  62. if mainImage
  63. images.push {
  64. url: mainImage
  65. type: 'main'
  66. order: 0
  67. filename: "main-" + Date.now() + ".jpg"
  68. }
  69. # Дополнительные изображения
  70. additionalImages = @getFieldByMapping(row, @fieldMapping, 'additionalImages')
  71. if additionalImages
  72. imageUrls = additionalImages.split('\n').slice(0, @maxImages)
  73. imageUrls.forEach (imgUrl, index) ->
  74. if imgUrl.trim()
  75. images.push {
  76. url: imgUrl.trim()
  77. type: 'additional'
  78. order: index + 1
  79. filename: "additional-" + index + "-" + Date.now() + ".jpg"
  80. }
  81. return images
  82. extractProductAttributes: (row, fieldMapping) ->
  83. attributes = {}
  84. # Все поля, не попавшие в маппинг, становятся атрибутами
  85. for field, value of row
  86. if value and not fieldMapping[field]
  87. attributes[field] = value.toString().trim()
  88. return attributes
  89. processProductsInBatches: (products, domain, onProgress) ->
  90. batches = []
  91. for i in [0...products.length] by @batchSize
  92. batches.push(products.slice(i, i + @batchSize))
  93. processed = 0
  94. results = {
  95. success: []
  96. errors: []
  97. }
  98. processBatch = (batch) =>
  99. try
  100. # Создание отсутствующих категорий
  101. await @ensureCategoriesExist(batch, domain)
  102. # Сохранение товаров
  103. batchResults = await ProductService.bulkSaveProducts(batch)
  104. results.success = results.success.concat(batchResults.success)
  105. results.errors = results.errors.concat(batchResults.errors)
  106. processed += batch.length
  107. # Прогресс
  108. if onProgress
  109. onProgress({
  110. processed: processed
  111. total: products.length
  112. percentage: Math.round((processed / products.length) * 100)
  113. results: results
  114. })
  115. return batchResults
  116. catch error
  117. log '❌ Ошибка обработки пакета:', error
  118. batchErrors = batch.map (product, index) =>
  119. { product: product, error: error.message, index: index }
  120. results.errors = results.errors.concat(batchErrors)
  121. return { success: [], errors: batchErrors }
  122. # Последовательная обработка пакетов
  123. for batch, index in batches
  124. await processBatch(batch, index)
  125. log "✅ Импорт завершен: Успешно " + results.success.length + ", Ошибок " + results.errors.length
  126. return results
  127. ensureCategoriesExist: (products, domain) ->
  128. categories = []
  129. products.forEach (product) ->
  130. if product.category and categories.indexOf(product.category) == -1
  131. categories.push(product.category)
  132. for categoryName in categories
  133. try
  134. slug = @slugify(categoryName)
  135. existingCategory = await CategoryService.getCategoryBySlug(slug)
  136. if not existingCategory
  137. category = new Category()
  138. category._id = "category:" + slug
  139. category.name = categoryName
  140. category.slug = slug
  141. category.domains = [domain]
  142. category.type = 'category'
  143. category.active = true
  144. category.order = 0
  145. await CategoryService.saveCategory(category)
  146. log "✅ Создана категория: " + categoryName
  147. catch error
  148. log "❌ Ошибка создания категории " + categoryName + ":", error
  149. slugify: (text) ->
  150. return '' unless text
  151. text.toString().toLowerCase()
  152. .replace(/\s+/g, '-')
  153. .replace(/[^\w\-]+/g, '')
  154. .replace(/\-\-+/g, '-')
  155. .replace(/^-+/, '')
  156. .replace(/-+$/, '')
  157. jsonToMarkdown: (richContent) ->
  158. # Преобразование JSON rich-контента в Markdown
  159. markdown = ''
  160. if richContent and richContent.content
  161. richContent.content.forEach (block) ->
  162. if block.widgetName == 'raTextBlock' and block.text and block.text.items
  163. block.text.items.forEach (item) ->
  164. if item.type == 'text' and item.content
  165. markdown += item.content + '\n\n'
  166. else if item.type == 'br'
  167. markdown += '\n'
  168. return markdown.trim()
  169. module.exports = new ImportService()