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:
Torsten Schulz (local)
2026-01-29 15:58:31 +01:00
parent c5ab17ad99
commit f1717920b6
5 changed files with 146 additions and 47 deletions

View File

@@ -214,6 +214,16 @@ class FalukantController {
} }
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId); 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.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId)); this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));

View File

@@ -82,6 +82,7 @@ router.get('/cities', falukantController.getRegions);
router.get('/products/price-in-region', falukantController.getProductPriceInRegion); router.get('/products/price-in-region', falukantController.getProductPriceInRegion);
router.get('/products/prices-in-region', falukantController.getAllProductPricesInRegion); router.get('/products/prices-in-region', falukantController.getAllProductPricesInRegion);
router.get('/products/prices-in-cities', falukantController.getProductPricesInCities); 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('/branches/:branchId/taxes', falukantController.getBranchTaxes);
router.get('/vehicles/types', falukantController.getVehicleTypes); router.get('/vehicles/types', falukantController.getVehicleTypes);
router.post('/vehicles', falukantController.buyVehicles); router.post('/vehicles', falukantController.buyVehicles);

View File

@@ -4528,6 +4528,103 @@ class FalukantService extends BaseService {
return results; 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) { async renovate(hashedUserId, element) {
const user = await getFalukantUserOrFail(hashedUserId); const user = await getFalukantUserOrFail(hashedUserId);
const house = await UserHouse.findOne({ const house = await UserHouse.findOne({

View File

@@ -114,36 +114,28 @@
} }
}, },
async loadPricesForAllProducts() { async loadPricesForAllProducts() {
if (this.currentRegionId === null || this.currentRegionId === undefined) { if (this.currentRegionId === null || this.currentRegionId === undefined || !this.products.length) {
return; return;
} }
const requests = this.products.map(async (product) => { const items = this.products.map((product) => ({
if (this.loadingPrices.has(product.id)) return; productId: product.id,
this.loadingPrices.add(product.id); currentPrice: parseFloat(this.calculateProductRevenue(product).absolute)
try { }));
const currentPrice = parseFloat(this.calculateProductRevenue(product).absolute); try {
const { data } = await apiClient.get('/api/falukant/products/prices-in-cities', { const { data } = await apiClient.post('/api/falukant/products/prices-in-cities-batch', {
params: { currentRegionId: this.currentRegionId,
productId: product.id, items
currentPrice: currentPrice, });
currentRegionId: this.currentRegionId this.betterPricesMap = { ...this.betterPricesMap };
} for (const product of this.products) {
}); this.betterPricesMap[product.id] = (data && data[product.id]) ? data[product.id] : [];
this.betterPricesMap = {
...this.betterPricesMap,
[product.id]: data || []
};
} catch (error) {
console.error(`Error loading prices for product ${product.id}:`, error);
this.betterPricesMap = {
...this.betterPricesMap,
[product.id]: []
};
} finally {
this.loadingPrices.delete(product.id);
} }
}); } catch (error) {
await Promise.all(requests); console.error('Error loading prices for products:', error);
for (const product of this.products) {
this.betterPricesMap = { ...this.betterPricesMap, [product.id]: [] };
}
}
}, },
getBetterPrices(productId) { getBetterPrices(productId) {
return this.betterPricesMap[productId] || []; return this.betterPricesMap[productId] || [];

View File

@@ -293,27 +293,26 @@
} }
}, },
async loadPricesForInventory() { async loadPricesForInventory() {
const requests = this.inventory.map(async (item) => { if (this.inventory.length === 0) return;
const itemKey = `${item.region.id}-${item.product.id}-${item.quality}`; const currentRegionId = this.inventory[0]?.region?.id ?? null;
if (this.loadingPrices.has(itemKey)) return; const items = this.inventory.map(item => ({
this.loadingPrices.add(itemKey); productId: item.product.id,
try { currentPrice: item.product.sellCost || 0
const currentPrice = item.product.sellCost || 0; }));
const { data } = await apiClient.get('/api/falukant/products/prices-in-cities', { try {
params: { const { data } = await apiClient.post('/api/falukant/products/prices-in-cities-batch', {
productId: item.product.id, currentRegionId,
currentPrice: currentPrice items
} });
}); for (const item of this.inventory) {
item.betterPrices = data || []; item.betterPrices = data && data[item.product.id] ? data[item.product.id] : [];
} catch (error) {
console.error(`Error loading prices for item ${itemKey}:`, error);
item.betterPrices = [];
} finally {
this.loadingPrices.delete(itemKey);
} }
}); } catch (error) {
await Promise.all(requests); console.error('Error loading prices for inventory:', error);
for (const item of this.inventory) {
item.betterPrices = [];
}
}
}, },
getCityPriceClass(branchType) { getCityPriceClass(branchType) {
if (branchType === 'store') return 'city-price-green'; if (branchType === 'store') return 'city-price-green';