Добавлены новые зависимости для работы с графиками и статистикой: интегрирован пакет recharts для визуализации данных. Обновлены компоненты бизнес-демо и сайдбара, добавлены новые функции для отображения информации о поставках и статистике. Улучшена структура кода и взаимодействие с пользователем. Обновлены GraphQL резолверы для получения статистики Wildberries.
This commit is contained in:
@ -61,143 +61,388 @@ interface WildberriesCardFilter {
|
||||
}
|
||||
}
|
||||
|
||||
export class WildberriesService {
|
||||
private static contentUrl = 'https://content-api.wildberries.ru'
|
||||
private static publicUrl = 'https://public-api.wildberries.ru'
|
||||
private static supplierUrl = 'https://suppliers-api.wildberries.ru'
|
||||
export interface WBSalesData {
|
||||
date: string
|
||||
saleID: string
|
||||
gNumber: string
|
||||
supplierArticle: string
|
||||
techSize: string
|
||||
barcode: string
|
||||
totalPrice: number
|
||||
discountPercent: number
|
||||
isSupply: boolean
|
||||
isRealization: boolean
|
||||
promoCodeDiscount: number
|
||||
warehouseName: string
|
||||
countryName: string
|
||||
oblastOkrugName: string
|
||||
regionName: string
|
||||
incomeID: number
|
||||
isCancel: boolean
|
||||
cancelDate: string
|
||||
orderType: string
|
||||
sticker: string
|
||||
srp: number
|
||||
}
|
||||
|
||||
export interface WBOrdersData {
|
||||
date: string
|
||||
lastChangeDate: string
|
||||
supplierArticle: string
|
||||
techSize: string
|
||||
barcode: string
|
||||
totalPrice: number
|
||||
discountPercent: number
|
||||
warehouseName: string
|
||||
oblast: string
|
||||
incomeID: number
|
||||
odid: number
|
||||
nmId: number
|
||||
subject: string
|
||||
category: string
|
||||
brand: string
|
||||
isCancel: boolean
|
||||
cancel_dt: string
|
||||
}
|
||||
|
||||
export interface WBAdvertData {
|
||||
advertId: number
|
||||
name: string
|
||||
status: number
|
||||
type: number
|
||||
createTime: string
|
||||
changeTime: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
dailyBudget: number
|
||||
budget: number
|
||||
}
|
||||
|
||||
export interface WBAdvertStatsRequest {
|
||||
id?: number
|
||||
dates?: string[]
|
||||
interval?: {
|
||||
begin: string
|
||||
end: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface WBAdvertStatsResponse {
|
||||
dates: string[]
|
||||
views: number
|
||||
clicks: number
|
||||
ctr: number
|
||||
cpc: number
|
||||
sum: number
|
||||
atbs: number
|
||||
orders: number
|
||||
cr: number
|
||||
shks: number
|
||||
sum_price: number
|
||||
days: Array<{
|
||||
date: string
|
||||
views: number
|
||||
clicks: number
|
||||
ctr: number
|
||||
cpc: number
|
||||
sum: number
|
||||
atbs: number
|
||||
orders: number
|
||||
cr: number
|
||||
shks: number
|
||||
sum_price: number
|
||||
}>
|
||||
advertId: number
|
||||
}
|
||||
|
||||
export interface WBStatisticsData {
|
||||
date: string
|
||||
sales: number
|
||||
orders: number
|
||||
advertising: number
|
||||
refusals: number
|
||||
returns: number
|
||||
revenue: number
|
||||
buyoutPercentage: number
|
||||
}
|
||||
|
||||
class WildberriesService {
|
||||
private apiKey: string
|
||||
private baseURL = 'https://statistics-api.wildberries.ru'
|
||||
private advertURL = 'https://advert-api.wildberries.ru'
|
||||
|
||||
constructor(apiKey: string) {
|
||||
this.apiKey = apiKey
|
||||
}
|
||||
|
||||
private async makeRequest<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Authorization': this.apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
|
||||
// Получение данных о продажах
|
||||
async getSales(dateFrom: string, flag = 0): Promise<WBSalesData[]> {
|
||||
const url = `${this.baseURL}/api/v1/supplier/sales?dateFrom=${dateFrom}&flag=${flag}`
|
||||
return this.makeRequest<WBSalesData[]>(url)
|
||||
}
|
||||
|
||||
// Получение данных о заказах
|
||||
async getOrders(dateFrom: string, flag = 0): Promise<WBOrdersData[]> {
|
||||
const url = `${this.baseURL}/api/v1/supplier/orders?dateFrom=${dateFrom}&flag=${flag}`
|
||||
return this.makeRequest<WBOrdersData[]>(url)
|
||||
}
|
||||
|
||||
// Получение рекламных кампаний
|
||||
async getAdverts(status?: number, type?: number, limit = 100, offset = 0): Promise<WBAdvertData[]> {
|
||||
let url = `${this.advertURL}/adv/v0/adverts?limit=${limit}&offset=${offset}`
|
||||
if (status) url += `&status=${status}`
|
||||
if (type) url += `&type=${type}`
|
||||
|
||||
return this.makeRequest<WBAdvertData[]>(url)
|
||||
}
|
||||
|
||||
// Получение статистики всех рекламных кампаний (v2)
|
||||
async getAdvertStats(dateFrom: string, dateTo: string): Promise<WBAdvertStatsResponse[]> {
|
||||
const url = `${this.advertURL}/adv/v2/fullstats`
|
||||
|
||||
// Попробуем через interval - это может работать лучше
|
||||
const request: WBAdvertStatsRequest[] = [{
|
||||
interval: {
|
||||
begin: dateFrom,
|
||||
end: dateTo
|
||||
}
|
||||
}]
|
||||
|
||||
console.log(`WB API: Requesting campaign stats with interval: ${dateFrom} to ${dateTo}`)
|
||||
console.log(`WB API: Request body:`, JSON.stringify(request, null, 2))
|
||||
|
||||
/**
|
||||
* Получение карточек товаров через Content API v2
|
||||
*/
|
||||
static async getCards(apiKey: string, filter?: WildberriesCardFilter): Promise<WildberriesCard[]> {
|
||||
try {
|
||||
console.log('Calling WB Content API v2 with filter:', filter)
|
||||
|
||||
const response = await fetch(`${this.contentUrl}/content/v2/get/cards/list`, {
|
||||
const response = await this.makeRequest<WBAdvertStatsResponse[]>(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(filter || {
|
||||
settings: {
|
||||
cursor: {
|
||||
limit: 100
|
||||
},
|
||||
filter: {
|
||||
withPhoto: -1
|
||||
}
|
||||
}
|
||||
})
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
|
||||
console.log(`WB API: Advert response:`, JSON.stringify(response, null, 2))
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error(`WB API: Advert stats error:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`${this.contentUrl}/content/v2/get/cards/list`, response.status, response.statusText)
|
||||
// Получение детального отчета по периоду
|
||||
async getDetailReport(dateFrom: string, dateTo: string, limit = 10000, rrdid = 0): Promise<Record<string, unknown>[]> {
|
||||
const url = `${this.baseURL}/api/v1/supplier/reportDetailByPeriod?dateFrom=${dateFrom}&dateTo=${dateTo}&limit=${limit}&rrdid=${rrdid}`
|
||||
return this.makeRequest<Record<string, unknown>[]>(url)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorData
|
||||
try {
|
||||
errorData = JSON.parse(errorText)
|
||||
} catch {
|
||||
errorData = { message: errorText }
|
||||
}
|
||||
// Агрегированная статистика для дашборда
|
||||
async getStatistics(dateFrom: string, dateTo: string): Promise<WBStatisticsData[]> {
|
||||
try {
|
||||
console.log(`WB API: Getting statistics from ${dateFrom} to ${dateTo}`)
|
||||
|
||||
// Получаем продажи и заказы
|
||||
const [salesData, ordersData] = await Promise.all([
|
||||
this.getSales(dateFrom, 0), // flag=0 для получения данных за период от dateFrom до сегодня
|
||||
this.getOrders(dateFrom, 0)
|
||||
])
|
||||
|
||||
console.log(`WB API: Got ${salesData.length} sales, ${ordersData.length} orders`)
|
||||
|
||||
// Получаем статистику рекламы напрямую через /adv/v2/fullstats
|
||||
let advertStatsData: WBAdvertStatsResponse[] = []
|
||||
try {
|
||||
console.log(`WB API: Getting campaign stats for interval: ${dateFrom} to ${dateTo}`)
|
||||
|
||||
console.log('WB API Error Response:', errorData)
|
||||
throw new Error(`WB API Error: ${response.status} - ${response.statusText}`)
|
||||
advertStatsData = await this.getAdvertStats(dateFrom, dateTo)
|
||||
console.log(`WB API: Got advertising stats for ${advertStatsData.length} campaigns`)
|
||||
|
||||
// Логируем детали рекламных затрат
|
||||
advertStatsData.forEach(stat => {
|
||||
const totalSpend = stat.days?.reduce((sum, day) => sum + (day.sum || 0), 0) || stat.sum || 0
|
||||
console.log(`WB API: Campaign ${stat.advertId} spent ${totalSpend} rubles over ${stat.days?.length || 0} days`)
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('WB API: Failed to get advertising stats:', error)
|
||||
}
|
||||
|
||||
const data: WildberriesCardsResponse = await response.json()
|
||||
return data.cards || []
|
||||
} catch (error) {
|
||||
console.error('Error fetching WB cards:', error)
|
||||
throw new Error('Ошибка получения карточек товаров')
|
||||
}
|
||||
}
|
||||
// Группируем данные по датам
|
||||
const statsMap = new Map<string, WBStatisticsData>()
|
||||
|
||||
/**
|
||||
* Поиск карточек товаров
|
||||
*/
|
||||
static async searchCards(apiKey: string, searchTerm: string, limit = 50): Promise<WildberriesCard[]> {
|
||||
const filter: WildberriesCardFilter = {
|
||||
settings: {
|
||||
cursor: {
|
||||
limit
|
||||
},
|
||||
filter: {
|
||||
textSearch: searchTerm,
|
||||
withPhoto: -1
|
||||
// Обрабатываем продажи
|
||||
salesData.forEach(sale => {
|
||||
const date = sale.date.split('T')[0] // Берем только дату без времени
|
||||
|
||||
// Строгая фильтрация по диапазону дат
|
||||
if (date < dateFrom || date > dateTo) return
|
||||
|
||||
if (!statsMap.has(date)) {
|
||||
statsMap.set(date, {
|
||||
date,
|
||||
sales: 0,
|
||||
orders: 0,
|
||||
advertising: 0,
|
||||
refusals: 0,
|
||||
returns: 0,
|
||||
revenue: 0,
|
||||
buyoutPercentage: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.getCards(apiKey, filter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех карточек товаров с пагинацией
|
||||
*/
|
||||
static async getAllCards(apiKey: string, limit = 100): Promise<WildberriesCard[]> {
|
||||
const filter: WildberriesCardFilter = {
|
||||
settings: {
|
||||
cursor: {
|
||||
limit
|
||||
},
|
||||
filter: {
|
||||
withPhoto: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.getCards(apiKey, filter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение складов WB
|
||||
*/
|
||||
static async getWarehouses(apiKey: string): Promise<WildberriesWarehouse[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.supplierUrl}/api/v3/warehouses`, {
|
||||
headers: {
|
||||
'Authorization': apiKey,
|
||||
const stats = statsMap.get(date)!
|
||||
if (!sale.isCancel) {
|
||||
stats.sales += 1
|
||||
stats.revenue += sale.totalPrice * (1 - sale.discountPercent / 100)
|
||||
} else {
|
||||
stats.returns += 1
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
// Обрабатываем заказы
|
||||
ordersData.forEach(order => {
|
||||
const date = order.date.split('T')[0]
|
||||
|
||||
// Строгая фильтрация по диапазону дат
|
||||
if (date < dateFrom || date > dateTo) return
|
||||
|
||||
if (!statsMap.has(date)) {
|
||||
statsMap.set(date, {
|
||||
date,
|
||||
sales: 0,
|
||||
orders: 0,
|
||||
advertising: 0,
|
||||
refusals: 0,
|
||||
returns: 0,
|
||||
revenue: 0,
|
||||
buyoutPercentage: 0
|
||||
})
|
||||
}
|
||||
|
||||
const data: WildberriesWarehousesResponse = await response.json()
|
||||
return data.data || []
|
||||
} catch (error) {
|
||||
console.error('Error fetching warehouses:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка валидности API ключа
|
||||
*/
|
||||
static async validateApiKey(apiKey: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.contentUrl}/content/v2/get/cards/list`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
settings: {
|
||||
cursor: {
|
||||
limit: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
const stats = statsMap.get(date)!
|
||||
if (!order.isCancel) {
|
||||
stats.orders += 1
|
||||
} else {
|
||||
stats.refusals += 1
|
||||
}
|
||||
})
|
||||
|
||||
return response.ok
|
||||
// Обрабатываем данные рекламы
|
||||
advertStatsData.forEach(advertStat => {
|
||||
console.log(`WB API: Processing advert ${advertStat.advertId}, total sum: ${advertStat.sum}`)
|
||||
|
||||
// Обрабатываем статистику по дням для каждой кампании
|
||||
if (advertStat.days && advertStat.days.length > 0) {
|
||||
advertStat.days.forEach(day => {
|
||||
const date = day.date
|
||||
|
||||
console.log(`WB API: Day ${date} - spent ${day.sum} rubles (campaign ${advertStat.advertId})`)
|
||||
|
||||
// Строгая фильтрация по диапазону дат
|
||||
if (date < dateFrom || date > dateTo) {
|
||||
console.log(`WB API: Skipping ${date} - outside range ${dateFrom} to ${dateTo}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!statsMap.has(date)) {
|
||||
statsMap.set(date, {
|
||||
date,
|
||||
sales: 0,
|
||||
orders: 0,
|
||||
advertising: 0,
|
||||
refusals: 0,
|
||||
returns: 0,
|
||||
revenue: 0,
|
||||
buyoutPercentage: 0
|
||||
})
|
||||
}
|
||||
|
||||
const stats = statsMap.get(date)!
|
||||
const adSpend = day.sum || 0
|
||||
stats.advertising += adSpend
|
||||
console.log(`WB API: Added ${adSpend} rubles to ${date}, total now: ${stats.advertising}`)
|
||||
})
|
||||
} else {
|
||||
console.log(`WB API: No daily data for campaign ${advertStat.advertId}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Вычисляем процент выкупов
|
||||
statsMap.forEach(stats => {
|
||||
if (stats.orders > 0) {
|
||||
// Ограничиваем процент выкупов до 100% максимум
|
||||
const percentage = Math.min(100, Math.round((stats.sales / stats.orders) * 100))
|
||||
stats.buyoutPercentage = percentage
|
||||
}
|
||||
})
|
||||
|
||||
// Получаем данные по рекламе (это более сложно, так как нужна отдельная статистика)
|
||||
// Пока используем заглушку, в реальности нужно вызвать отдельный API
|
||||
|
||||
return Array.from(statsMap.values()).sort((a, b) => a.date.localeCompare(b.date))
|
||||
} catch (error) {
|
||||
console.error('Error validating API key:', error)
|
||||
return false
|
||||
console.error('Error fetching WB statistics:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Получение статистики рекламы (заглушка)
|
||||
async getAdvertStatistics(dateFrom: string, dateTo: string): Promise<{ date: string; spend: number }[]> {
|
||||
// В реальности здесь нужно вызвать соответствующий API для статистики рекламы
|
||||
// Пока возвращаем заглушку
|
||||
return []
|
||||
}
|
||||
|
||||
// Генерирует массив дат между dateFrom и dateTo
|
||||
private generateDateRange(dateFrom: string, dateTo: string): string[] {
|
||||
const dates: string[] = []
|
||||
const start = new Date(dateFrom)
|
||||
const end = new Date(dateTo)
|
||||
|
||||
const current = new Date(start)
|
||||
while (current <= end) {
|
||||
dates.push(WildberriesService.formatDate(current))
|
||||
current.setDate(current.getDate() + 1)
|
||||
}
|
||||
|
||||
return dates
|
||||
}
|
||||
|
||||
// Форматирование даты для WB API (RFC3339)
|
||||
static formatDate(date: Date): string {
|
||||
return date.toISOString().split('T')[0] // YYYY-MM-DD
|
||||
}
|
||||
|
||||
// Получение даты период назад
|
||||
static getDatePeriodAgo(period: 'week' | 'month' | 'quarter'): string {
|
||||
const now = new Date()
|
||||
const date = new Date(now)
|
||||
|
||||
switch (period) {
|
||||
case 'week':
|
||||
date.setDate(date.getDate() - 7)
|
||||
break
|
||||
case 'month':
|
||||
date.setMonth(date.getMonth() - 1)
|
||||
break
|
||||
case 'quarter':
|
||||
date.setMonth(date.getMonth() - 3)
|
||||
break
|
||||
}
|
||||
|
||||
return this.formatDate(date)
|
||||
}
|
||||
}
|
||||
|
||||
export { WildberriesService }
|
Reference in New Issue
Block a user