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);
|
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));
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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] || [];
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user