Add batch processing for product price retrieval: Implement getProductPricesInCitiesBatch method in FalukantService for handling multiple product price requests in a single API call. Update FalukantController and router to support new endpoint, and refactor RevenueSection and SaleSection components to utilize batch processing for improved performance and reduced API calls.
This commit is contained in:
@@ -214,6 +214,16 @@ class FalukantController {
|
||||
}
|
||||
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId);
|
||||
});
|
||||
this.getProductPricesInCitiesBatch = this._wrapWithUser((userId, req) => {
|
||||
const body = req.body || {};
|
||||
const items = Array.isArray(body.items) ? body.items : [];
|
||||
const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null;
|
||||
const valid = items.map(i => ({
|
||||
productId: parseInt(i.productId, 10),
|
||||
currentPrice: parseFloat(i.currentPrice)
|
||||
})).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice));
|
||||
return this.service.getProductPricesInCitiesBatch(userId, valid, Number.isNaN(currentRegionId) ? null : currentRegionId);
|
||||
});
|
||||
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
|
||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ router.get('/cities', falukantController.getRegions);
|
||||
router.get('/products/price-in-region', falukantController.getProductPriceInRegion);
|
||||
router.get('/products/prices-in-region', falukantController.getAllProductPricesInRegion);
|
||||
router.get('/products/prices-in-cities', falukantController.getProductPricesInCities);
|
||||
router.post('/products/prices-in-cities-batch', falukantController.getProductPricesInCitiesBatch);
|
||||
router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes);
|
||||
router.get('/vehicles/types', falukantController.getVehicleTypes);
|
||||
router.post('/vehicles', falukantController.buyVehicles);
|
||||
|
||||
@@ -4528,6 +4528,103 @@ class FalukantService extends BaseService {
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch-Variante: Preise für mehrere Produkte in einem Request.
|
||||
* @param {string} hashedUserId
|
||||
* @param {Array<{ productId: number, currentPrice: number }>} items
|
||||
* @param {number|null} currentRegionId
|
||||
* @returns {Promise<Record<number, Array<{ regionId, regionName, price, branchType }>>>}
|
||||
*/
|
||||
async getProductPricesInCitiesBatch(hashedUserId, items, currentRegionId = null) {
|
||||
if (!items || items.length === 0) return {};
|
||||
const productIds = [...new Set(items.map(i => i.productId))];
|
||||
const priceByProduct = new Map(items.map(i => [i.productId, i.currentPrice]));
|
||||
|
||||
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
||||
if (!character) {
|
||||
throw new Error(`No FalukantCharacter found for user with id ${user.id}`);
|
||||
}
|
||||
|
||||
const [products, knowledges, cities, townWorths] = await Promise.all([
|
||||
ProductType.findAll({ where: { id: { [Op.in]: productIds } }, attributes: ['id', 'sellCost'] }),
|
||||
Knowledge.findAll({
|
||||
where: { characterId: character.id, productId: { [Op.in]: productIds } },
|
||||
attributes: ['productId', 'knowledge']
|
||||
}),
|
||||
RegionData.findAll({
|
||||
attributes: ['id', 'name'],
|
||||
include: [
|
||||
{
|
||||
model: RegionType,
|
||||
as: 'regionType',
|
||||
where: { labelTr: 'city' },
|
||||
attributes: ['labelTr']
|
||||
},
|
||||
{
|
||||
model: Branch,
|
||||
as: 'branches',
|
||||
where: { falukantUserId: user.id },
|
||||
include: [{ model: BranchType, as: 'branchType', attributes: ['labelTr'] }],
|
||||
attributes: ['branchTypeId'],
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}),
|
||||
TownProductWorth.findAll({
|
||||
where: { productId: { [Op.in]: productIds } },
|
||||
attributes: ['productId', 'regionId', 'worthPercent']
|
||||
})
|
||||
]);
|
||||
|
||||
const knowledgeByProduct = new Map(knowledges.map(k => [k.productId, k.knowledge || 0]));
|
||||
const worthByProductRegion = new Map();
|
||||
for (const tw of townWorths) {
|
||||
const key = `${tw.productId}-${tw.regionId}`;
|
||||
worthByProductRegion.set(key, tw.worthPercent);
|
||||
}
|
||||
const productById = new Map(products.map(p => [p.id, p]));
|
||||
|
||||
const PRICE_TOLERANCE = 0.01;
|
||||
const out = {};
|
||||
|
||||
for (const product of products) {
|
||||
const knowledgeFactor = knowledgeByProduct.get(product.id) || 0;
|
||||
const currentPrice = priceByProduct.get(product.id) ?? product.sellCost ?? 0;
|
||||
let currentRegionalPrice = currentPrice;
|
||||
if (currentRegionId) {
|
||||
const wp = worthByProductRegion.get(`${product.id}-${currentRegionId}`) ?? 50;
|
||||
currentRegionalPrice = calcRegionalSellPriceSync(product, knowledgeFactor, wp) ?? currentPrice;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const city of cities) {
|
||||
if (currentRegionId && city.id === currentRegionId) continue;
|
||||
const worthPercent = worthByProductRegion.get(`${product.id}-${city.id}`) ?? 50;
|
||||
const priceInCity = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent);
|
||||
if (priceInCity == null) continue;
|
||||
if (priceInCity <= currentRegionalPrice - PRICE_TOLERANCE) continue;
|
||||
|
||||
let branchType = null;
|
||||
if (city.branches && city.branches.length > 0) {
|
||||
const branchTypes = city.branches.map(b => b.branchType?.labelTr).filter(Boolean);
|
||||
if (branchTypes.includes('store') || branchTypes.includes('fullstack')) branchType = 'store';
|
||||
else if (branchTypes.includes('production')) branchType = 'production';
|
||||
}
|
||||
results.push({
|
||||
regionId: city.id,
|
||||
regionName: city.name,
|
||||
price: priceInCity,
|
||||
branchType
|
||||
});
|
||||
}
|
||||
results.sort((a, b) => b.price - a.price);
|
||||
out[product.id] = results;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
async renovate(hashedUserId, element) {
|
||||
const user = await getFalukantUserOrFail(hashedUserId);
|
||||
const house = await UserHouse.findOne({
|
||||
|
||||
@@ -114,36 +114,28 @@
|
||||
}
|
||||
},
|
||||
async loadPricesForAllProducts() {
|
||||
if (this.currentRegionId === null || this.currentRegionId === undefined) {
|
||||
if (this.currentRegionId === null || this.currentRegionId === undefined || !this.products.length) {
|
||||
return;
|
||||
}
|
||||
const requests = this.products.map(async (product) => {
|
||||
if (this.loadingPrices.has(product.id)) return;
|
||||
this.loadingPrices.add(product.id);
|
||||
try {
|
||||
const currentPrice = parseFloat(this.calculateProductRevenue(product).absolute);
|
||||
const { data } = await apiClient.get('/api/falukant/products/prices-in-cities', {
|
||||
params: {
|
||||
const items = this.products.map((product) => ({
|
||||
productId: product.id,
|
||||
currentPrice: currentPrice,
|
||||
currentRegionId: this.currentRegionId
|
||||
}
|
||||
currentPrice: parseFloat(this.calculateProductRevenue(product).absolute)
|
||||
}));
|
||||
try {
|
||||
const { data } = await apiClient.post('/api/falukant/products/prices-in-cities-batch', {
|
||||
currentRegionId: this.currentRegionId,
|
||||
items
|
||||
});
|
||||
this.betterPricesMap = {
|
||||
...this.betterPricesMap,
|
||||
[product.id]: data || []
|
||||
};
|
||||
this.betterPricesMap = { ...this.betterPricesMap };
|
||||
for (const product of this.products) {
|
||||
this.betterPricesMap[product.id] = (data && data[product.id]) ? data[product.id] : [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading prices for product ${product.id}:`, error);
|
||||
this.betterPricesMap = {
|
||||
...this.betterPricesMap,
|
||||
[product.id]: []
|
||||
};
|
||||
} finally {
|
||||
this.loadingPrices.delete(product.id);
|
||||
console.error('Error loading prices for products:', error);
|
||||
for (const product of this.products) {
|
||||
this.betterPricesMap = { ...this.betterPricesMap, [product.id]: [] };
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(requests);
|
||||
},
|
||||
getBetterPrices(productId) {
|
||||
return this.betterPricesMap[productId] || [];
|
||||
|
||||
@@ -293,27 +293,26 @@
|
||||
}
|
||||
},
|
||||
async loadPricesForInventory() {
|
||||
const requests = this.inventory.map(async (item) => {
|
||||
const itemKey = `${item.region.id}-${item.product.id}-${item.quality}`;
|
||||
if (this.loadingPrices.has(itemKey)) return;
|
||||
this.loadingPrices.add(itemKey);
|
||||
try {
|
||||
const currentPrice = item.product.sellCost || 0;
|
||||
const { data } = await apiClient.get('/api/falukant/products/prices-in-cities', {
|
||||
params: {
|
||||
if (this.inventory.length === 0) return;
|
||||
const currentRegionId = this.inventory[0]?.region?.id ?? null;
|
||||
const items = this.inventory.map(item => ({
|
||||
productId: item.product.id,
|
||||
currentPrice: currentPrice
|
||||
}
|
||||
currentPrice: item.product.sellCost || 0
|
||||
}));
|
||||
try {
|
||||
const { data } = await apiClient.post('/api/falukant/products/prices-in-cities-batch', {
|
||||
currentRegionId,
|
||||
items
|
||||
});
|
||||
item.betterPrices = data || [];
|
||||
for (const item of this.inventory) {
|
||||
item.betterPrices = data && data[item.product.id] ? data[item.product.id] : [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading prices for item ${itemKey}:`, error);
|
||||
console.error('Error loading prices for inventory:', error);
|
||||
for (const item of this.inventory) {
|
||||
item.betterPrices = [];
|
||||
} finally {
|
||||
this.loadingPrices.delete(itemKey);
|
||||
}
|
||||
});
|
||||
await Promise.all(requests);
|
||||
}
|
||||
},
|
||||
getCityPriceClass(branchType) {
|
||||
if (branchType === 'store') return 'city-price-green';
|
||||
|
||||
Reference in New Issue
Block a user