This commit is contained in:
seb
2026-08-15 04:11:54 +02:00
parent e1a009ba5c
commit c42a14a437
7 changed files with 50 additions and 18 deletions

2
API.md
View File

@@ -296,7 +296,7 @@ No `limit` — returns all groups newer than the cursor.
### `GET /v1/customer`
Customer deltas. Only customers of customer group `1` are returned, scoped to the active shop (`vCustomer.kShop = active kShop`) and its subshop (`tInetKundeShop.kSubShop = active kShopSubshop`). Rows are ordered by `bLastChanged` ascending, and only rows with `bLastChanged <= nMaxLastChanged` of the active subshop are returned.
Customer deltas. Customers are scoped to the active shop (`vCustomer.kShop = active kShop`) and its subshop (`tInetKundeShop.kSubShop = active kShopSubshop`). Rows are ordered by `bLastChanged` ascending, and only rows with `bLastChanged > lastChangedCustomer` are returned (matching the count semantics of `/v1/init`).
| Param | Default |
|---|---|

View File

@@ -9,8 +9,7 @@ void handle_customer(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/
int limit = req.get_query_int("limit", 20);
int shop = get_active_shop_id();
int subshop = get_active_shop_subshop_id();
int64_t max_last_changed = get_active_max_last_changed();
auto customers = get_customer_list(cursor, limit, shop, subshop, max_last_changed);
auto customers = get_customer_list(cursor, limit, shop, subshop);
resp.send_json(200, customers);
}

View File

@@ -43,8 +43,7 @@ static const char* CUSTOMER_COUNT_SQL =
"SELECT COUNT(DISTINCT k.kKunde) AS cnt FROM dbo.tkunde k "
"LEFT JOIN dbo.tInetKundeShop iks ON iks.kKunde = k.kKunde "
"AND iks.kShop = ? AND iks.kSubShop = ? "
"WHERE k.kKundenGruppe = 1 "
"AND (? = 0 OR EXISTS (SELECT 1 FROM dbo.tInetKundeShop x "
"WHERE (? = 0 OR EXISTS (SELECT 1 FROM dbo.tInetKundeShop x "
"WHERE x.kKunde = k.kKunde AND x.kShop = ?)) "
"AND CONVERT(BIGINT, k.bRowversion) > ?";

View File

@@ -1,7 +1,9 @@
#pragma once
#include "../db/pool.hpp"
#include "../log.hpp"
#include "nlohmann/json.hpp"
#include "shop.hpp"
#include <cstdio>
static const char* CUSTOMER_LIST_SQL =
"SELECT TOP (?) "
@@ -37,8 +39,6 @@ static const char* CUSTOMER_LIST_SQL =
" AND tInetKundeShop.kSubShop = ? "
"WHERE vCustomer.kShop = ? "
" AND CONVERT(BIGINT, vCustomer.bLastChanged) > ? "
" AND CONVERT(BIGINT, vCustomer.bLastChanged) <= ? "
" AND vCustomer.kCustomerGroupId = 1 "
"ORDER BY vCustomer.bLastChanged ASC";
inline std::string format_birthday(const std::string& c_date_of_birth) {
@@ -56,13 +56,12 @@ inline std::string format_birthday(const std::string& c_date_of_birth) {
return buf;
}
inline nlohmann::json get_customer_list(int64_t cursor, int limit, int shop, int subshop, int64_t max_last_changed) {
inline nlohmann::json get_customer_list(int64_t cursor, int limit, int shop, int subshop) {
std::vector<Param> ps = {
{ParamType::Int, "", limit},
{ParamType::Int, "", subshop},
{ParamType::Int, "", shop},
{ParamType::BigInt, "", cursor},
{ParamType::BigInt, "", max_last_changed},
};
ResultSet rs;
if (!get_pool().execute(CUSTOMER_LIST_SQL, ps, rs)) {

View File

@@ -11,8 +11,7 @@ LEFT JOIN dbo.tInetKundeShop iks
ON iks.kKunde = k.kKunde
AND iks.kShop = @kShop
AND iks.kSubShop = @SubShopId
WHERE k.kKundenGruppe = 1
AND (@kShop = 0 OR EXISTS (
WHERE (@kShop = 0 OR EXISTS (
SELECT 1 FROM dbo.tInetKundeShop x
WHERE x.kKunde = k.kKunde AND x.kShop = @kShop
))

View File

@@ -2,7 +2,7 @@ import sql from 'mssql';
import { isDemoMode } from '../demo/mode.js';
import { getDemoCustomerList } from '../demo/store.js';
import { getPool } from '../db.js';
import { getActiveMaxLastChanged, getActiveShopId, getActiveShopSubshopId } from '../shop.js';
import { getActiveShopId, getActiveShopSubshopId } from '../shop.js';
const CUSTOMER_LIST_SQL = `
SELECT TOP (@Limit)
@@ -38,8 +38,6 @@ LEFT JOIN dbo.tInetKundeShop
AND tInetKundeShop.kSubShop = @SubShopId
WHERE vCustomer.kShop = @ShopId
AND CONVERT(BIGINT, vCustomer.bLastChanged) > @bLastChanged
AND CONVERT(BIGINT, vCustomer.bLastChanged) <= @MaxLastChanged
AND vCustomer.kCustomerGroupId = 1
ORDER BY vCustomer.bLastChanged ASC;
`;
@@ -72,7 +70,6 @@ export async function getCustomerList({ cursor = 0, limit = 20 } = {}) {
.input('ShopId', sql.Int, getActiveShopId())
.input('SubShopId', sql.Int, getActiveShopSubshopId())
.input('bLastChanged', sql.BigInt, cursor)
.input('MaxLastChanged', sql.BigInt, getActiveMaxLastChanged())
.query(CUSTOMER_LIST_SQL);
return result.recordset.map((row) => ({

View File

@@ -82,6 +82,26 @@ function priceOverridesSql(articleIds) {
`;
}
// Returns the first image hash (ordered by nNr) per article, matching the main
// query's imgHash selection. Used to resolve the parent's image for products
// without a picture of their own.
function imageHashesSql(articleIds) {
const idList = articleIds.join(',');
return `
SELECT articleId, imgHash
FROM (
SELECT
abp.kArtikel AS articleId,
img.cHash AS imgHash,
ROW_NUMBER() OVER (PARTITION BY abp.kArtikel ORDER BY abp.nNr) AS rn
FROM dbo.tArtikelbildPlattform abp
INNER JOIN dbo.tBild img ON img.kBild = abp.kBild
WHERE abp.kArtikel IN (${idList})
) t
WHERE rn = 1 AND imgHash IS NOT NULL AND imgHash <> '';
`;
}
function formatDateTime(date) {
if (!date) {
return '0001-01-01 00:00:00';
@@ -118,10 +138,20 @@ export async function getProductList({ cursor = 0, limit = 20 } = {}) {
const overridesByArticle = new Map();
let attributesByArticle = new Map();
let parentImageHashes = new Map();
if (articleIds.length > 0) {
const [overrideResult, attributeMap] = await Promise.all([
// Include any parent articles referenced by products in this batch so we
// can resolve the parent image for products without a picture of their own,
// even when the parent itself is not part of the batch.
const parentIds = products
.map((p) => (p.parentArticleId > 0 && !p.imgHash ? Number(p.parentArticleId) : null))
.filter((id) => id !== null && !articleIds.includes(id));
const hashIds = [...new Set([...articleIds, ...parentIds])];
const [overrideResult, attributeMap, imageHashResult] = await Promise.all([
pool.request().query(priceOverridesSql(articleIds)),
getProductAttributes(pool, articleIds),
pool.request().query(imageHashesSql(hashIds)),
]);
attributesByArticle = attributeMap;
for (const row of overrideResult.recordset) {
@@ -130,6 +160,9 @@ export async function getProductList({ cursor = 0, limit = 20 } = {}) {
}
overridesByArticle.get(row.articleId).set(row.customerGroupId, row.netPrice);
}
for (const row of imageHashResult.recordset) {
parentImageHashes.set(row.articleId, row.imgHash);
}
}
return products.map((product) => {
@@ -151,10 +184,16 @@ export async function getProductList({ cursor = 0, limit = 20 } = {}) {
const categoryIds = product.categoryIds ? product.categoryIds.split(',') : [];
const articleAttributes = attributesByArticle.get(product.id);
// If a product has a parent and no picture of its own, fall back to the
// parent's image hash.
const hasOwnImage = !!product.imgHash;
const parentHash = !hasOwnImage ? parentImageHashes.get(product.parentArticleId) : null;
const imageHash = hasOwnImage ? product.imgHash : parentHash;
return {
_id: String(product.id),
imghash: product.imgHash ?? null,
imgsrc: product.imgHash ?? null,
imghash: imageHash ?? null,
imgsrc: imageHash ?? null,
sku: product.sku,
barcode: product.barcode ?? null,
name: product.name,