Կատալոգ, զամբյուղ և checkout
Ցանկացած առցանց խանութի ողնաշարը՝ ապրանքներ, գներ, զամբյուղներ և պատվերներ, որոնք երբեք չեն ստում։
30 վայրկյանում
Առցանց խանութը իրար կպցված երեք համակարգ է՝ կատալոգ, որը նկարագրում է, թե ինչ եք վաճառում, զամբյուղ, որը հիշում է, թե ինչ է ուզում հաճախորդը, և checkout, որը այդ ցանկությունը վերածում է վճարված, հետևելի պատվերի։ Եթե տվյալների մոդելը ճիշտ կառուցեք հենց սկզբից՝ տարբերակներ (variants), գումարը որպես ամբողջ թիվ, պատվերի բացահայտ state machine, հետագայում կխնայեք ամիսներ ցավոտ ուղղումների վրա։
Ինչու եմ օգտագործում
- Ես օգտագործում եմ այս կառուցվածքը, որովհետև այն կառուցել եմ նորից ու նորից․ ռեստորանային պատվերներ, կենդանիների ապրանքներ, գեղեցկության ապրանքներ, գինի, merch, սնունդ և տուրեր՝ բոլորն էլ հանգում են նույն կատալոգ → զամբյուղ → պատվեր շղթային։
- Խանութների production-ի սխալների մեծ մասը էկզոտիկ չէ՝ կլորացման սխալ գնի մեջ, զամբյուղ, որը login-ից հետո կորցնում է ապրանքները, երկու անգամ վճարված պատվեր, վաճառված ապրանք, որն այլևս պահեստում չկա։ Մաքուր մոդելը վերացնում է դրանց ամբողջ դասեր։
- Checkout-ն այն տեղն է, որտեղ բիզնեսը գումար է վաստակում։ Ամեն ավելորդ քայլ կամ անհասկանալի սխալ իրական պատվերներ է արժենում, ուստի այն արժանի է senior մակարդակի ուշադրության։
- Երբ միջուկը կայուն է, ակցիաները, լոյալության ծրագիրը, ERP սինքրոնիզացիան և հավելվածները կարելի է ավելացնել վրան՝ առանց այն վերաշարադրելու։
Ինչպես է աշխատում
01Կատալոգը՝ ապրանքներ, տարբերակներ և ատրիբուտներ
Պատկերացրեք ապրանքը որպես դարակի պիտակ («Եղունգների լաք»), իսկ տարբերակը (variant)՝ որպես այն իրական իրը, որը դնում եք տուփի մեջ («Եղունգների լաք, կարմիր, 15 ml»)։ Հաճախորդները դիտում են ապրանքները, բայց դուք վաճառում, պահում և գնագոյացնում եք տարբերակները․ յուրաքանչյուր տարբերակ ունի իր SKU-ն (stock-keeping unit՝ եզակի կոդ), գինը և մնացորդը։ Ատրիբուտները նկարագրում են տարբերակները՝ գույն, չափս, քաշ, համ։ Ատրիբուտները ատրիբուտների ընտանիքների մեջ խմբավորելը («հագուստն» ունի չափս և գույն, «կենդանիների կերը»՝ քաշ և համ) ադմինը կարգին է պահում։ Կատեգորիաները կազմում են ծառ (Շներ → Կեր → Չոր կեր)․ պահեք դրանք nested-set-ով կամ parent_id-ով և cache-ավորված ճանապարհով (path), որպեսզի «Շներ-ի տակ գտնվող բոլոր ապրանքները» լինի մեկ query։ Բրենդները, թեգերը և հավաքածուները առանձին many-to-many կապեր են, ոչ թե լրացուցիչ կատեգորիաներ։
- Ապրանք՝ անուն, նկարագրություն, նկարներ, SEO դաշտեր, կապեր կատեգորիաների հետ։
- Տարբերակ՝ SKU, ատրիբուտների արժեքներ, գին, քաշ, շտրիխ-կոդ, կապ մնացորդի հետ։
- Որոշ ապրանքներ վաճառվում են տուփերով կամ քաշով․ վաճառքի միավորը մոդելավորեք բացահայտ՝ քանակների հետ հնարքներ անելու փոխարեն։
02Գումար՝ ամբողջ թվեր, արժույթներ և հարկեր
Երբեք մի պահեք գումարը float-ով։ Երկուական լողացող ստորակետով 0.1 + 0.2-ը 0.3 չէ, և այդ փոքրիկ սխալները գումարվում են ու դառնում հաշվապահական իրական տարբերություններ։ Պահեք գումարները ամբողջ թվերով՝ ամենափոքր միավորով (ցենտեր, կամ ամբողջ դրամներ այն արժույթների համար, որոնք մանր միավոր չունեն), ISO 4217 արժույթի կոդի հետ միասին, և թվաբանությունն արեք money գրադարանով կամ value object-ով։ Մեկ անգամ որոշեք՝ կատալոգի գներն ներառո՞ւմ են հարկը (տարածված է Եվրոպայի, Հայաստանի և Ավստրալիայի սպառողական խանութներում), թե՞ ոչ (տարածված է ԱՄՆ-ում), և հարկը հաշվարկեք յուրաքանչյուր տողի համար՝ կլորացման հստակ կանոնով։ Պատվերի տողի վրա պահեք յուրաքանչյուր գնի, զեղչի և հարկի snapshot-ը․ կատալոգը վաղը կփոխվի, բայց պատվերը դեռ պետք է ցույց տա, թե ինչ է վճարել հաճախորդը։
// Money as a small value object: integer minor units + currency
type Money = { amount: number; currency: "USD" | "EUR" | "AMD" };
const add = (a: Money, b: Money): Money => {
if (a.currency !== b.currency) throw new Error("Currency mismatch");
return { amount: a.amount + b.amount, currency: a.currency };
};
// Tax included in the price: extract it with one explicit rounding rule
const taxIncluded = (gross: Money, ratePercent: number): Money => ({
amount: Math.round((gross.amount * ratePercent) / (100 + ratePercent)),
currency: gross.currency,
});
const line = { amount: 1999, currency: "USD" } as const; // $19.99
console.log(taxIncluded(line, 20)); // { amount: 333, currency: "USD" }
03Զամբյուղը՝ հյուրեր, միավորում և վալիդացիա
Թույլ տվեք մարդկանց գնումներ անել առանց հաշվի։ Հյուրի զամբյուղն ապրում է սերվերում և նույնականացվում է պատահական token-ով, որը պահվում է cookie-ում (վեբ) կամ secure storage-ում (հավելված)։ Սերվերում պահելը նշանակում է, որ գներն ու կանոնները հաշվարկվում են մեկ տեղում, և login-ից հետո զամբյուղը հետևում է օգտատիրոջը տարբեր սարքերում։ Երբ հյուրը մուտք է գործում, միավորեք՝ հյուրի տողերը ավելացրեք հաշվի զամբյուղին, նույն տարբերակի քանակները գումարեք, ապա ջնջեք հյուրի զամբյուղը։ Զամբյուղին վերաբերվեք որպես ցանկության, ոչ թե խոստման։ Ամեն անգամ, երբ այն կարդացվում է, և միշտ checkout-ի ժամանակ, նորից վալիդացրեք այն․ տարբերակը դեռ ակտի՞վ է, մնացորդը բավարա՞ր է, գինը փոխվե՞լ է, մասնաճյուղը բա՞ց է, նվազագույն պատվերի գումարը ապահովվա՞ծ է։ Վերադարձրեք հստակ հաղորդագրություններ («Գինը փոխվել է 12.00-ից 13.50»)՝ լուռ ուղղելու փոխարեն։
- Զամբյուղում պահեք տարբերակների ID-ներն ու քանակները, երբեք մի վստահեք client-ից եկած գներին։
- Յուրաքանչյուր response-ի համար ընդհանուր գումարները վերահաշվարկեք սերվերում։
- Առաջարկեք կրկնել պատվերը՝ անցյալ պատվերի տողերը պատճենեք զամբյուղ, ապա վալիդացրեք սովորականի պես։
04Checkout և պատվերի state machine
Checkout-ը հավաքում է այն, ինչ պետք է պատվերին՝ կոնտակտ, հասցե կամ վերցնելու կետ, առաքման ժամային միջակայք, վճարման եղանակ, ապա ստեղծում է պատվերը մեկ տվյալների բազայի transaction-ում՝ պատճենում է զամբյուղի տողերը գների snapshot-ներով, ռեզերվում է մնացորդը, պահում ընդհանուր գումարները և կարգավիճակը դնում «սպասում է վճարման»։ Այդ պահից պատվերը շարժվում է բացահայտ state machine-ով։ State machine-ը պարզապես թույլատրված կարգավիճակների և դրանց միջև թույլատրված անցումների ցուցակ է։ Այն գրի առնելը կանխում է անհեթեթություններ, օրինակ՝ չեղարկված պատվերը դառնում է առաքված, և յուրաքանչյուր անցում գրանցվում է status history աղյուսակում՝ ով և երբ։ Կողմնակի էֆեկտները՝ email-ներ, push, խոհանոցի էկրաններ, ERP export, կախված են անցումներից, իդեալում՝ queue-ի job-երի միջոցով։
const transitions = {
pending_payment: ["paid", "cancelled"],
paid: ["preparing", "refunded"],
preparing: ["ready", "cancelled"],
ready: ["out_for_delivery", "picked_up"],
out_for_delivery: ["delivered", "failed_delivery"],
delivered: ["refunded"],
} as const;
type Status = keyof typeof transitions | "cancelled" | "refunded" | "picked_up" | "failed_delivery";
export function canMove(from: Status, to: Status): boolean {
const allowed = (transitions as Record<string, readonly string[]>)[from] ?? [];
return allowed.includes(to);
}
05Առաքում, վերցնում տեղից և ժամային միջակայքեր
Fulfilment-ն այն տեղն է, որտեղ խանութներն ամենաշատն են տարբերվում։ Առաքման համար պետք է հասցե, գոտի և վճար։ Գոտիները սովորաբար քարտեզի վրա պոլիգոններ են․ geocode արեք հասցեն կոորդինատների, ապա ստուգեք, թե որ պոլիգոնն է պարունակում կետը, որպեսզի ստանաք վճարը, նվազագույն պատվերը և սպասարկող մասնաճյուղը։ Տեղից վերցնելու (takeaway) համար պետք է մասնաճյուղ և գուցե ապրանքների ցուցակ, որը տարբերվում է ըստ մասնաճյուղի։ Ժամային միջակայքերը աշխատանքային ժամերը վերածում են ընտրելի պատուհանների՝ «հնարավորինս շուտ» կամ պլանավորված՝ 15 կամ 30 րոպե քայլով, հաշվի առնելով պատրաստման ժամանակը, փակման ժամը և «աշխատանքային օրը», որը կարող է ավարտվել կեսգիշերից հետո։ Միջակայքերը գեներացրեք սերվերում՝ խանութի ժամային գոտով, և ընտրված միջակայքը checkout-ում նորից վալիդացրեք․ միջակայքը, որն ազատ էր էջը բեռնելիս, մեկ րոպե անց կարող է արդեն զբաղված լինել։
06Overselling-ի կանխում և ճիշտ email-ների ուղարկում
Overselling-ը տեղի է ունենում, երբ երկու հաճախորդ նույն պահին գնում են վերջին ապրանքը։ Լուծումը մնացորդի փոփոխությունն ատոմար դարձնելն է։ Կամ lock արեք տողը (SELECT … FOR UPDATE transaction-ի ներսում), կամ օգտագործեք պայմանական update, որը հաջողվում է միայն այն դեպքում, երբ բավարար մնացորդ կա, և ստուգեք ազդված տողերի քանակը։ Ռեզերվեք մնացորդը պատվերը ստեղծելիս և ազատեք այն, եթե վճարումը ձախողվի կամ ժամանակը լրանա․ պլանավորված job-ը չեղարկում է լքված, վճարման սպասող պատվերները։ Transactional email-ները՝ պատվերն ընդունված է, վճարված է, ուղարկված է, գումարը վերադարձված է, պետք է ուղարկվեն queue-ից transaction-ի commit-ից հետո, որպեսզի դանդաղ mail սերվերը երբեք չարգելափակի checkout-ը, և email չուղարկվի rollback արված պատվերի համար։
-- Atomic decrement: succeeds only when enough stock is left
UPDATE stock_levels
SET quantity = quantity - 2
WHERE variant_id = 42
AND warehouse_id = 1
AND quantity >= 2;
-- 1 row affected → reserved; 0 rows → out of stock, tell the customer
Օգտագործեք, երբ
- Դուք կառուցում եք խանութ իրական բիզնես կանոններով (տարբերակներ, առաքման գոտիներ, ակցիաներ, ERP), որոնց դեմ պատրաստի պլատֆորմները պայքարում են։
- Նույն կատալոգը պետք է սպասարկի կայքը, մոբայլ հավելվածը և ադմինը մեկ API-ից։
- Պատվերները պետք է ինտեգրվեն հաշվապահության, ֆիսկալ կտրոնների, խոհանոցների կամ պահեստների հետ։
- Ձեզ պետք է checkout-ի UX-ի և տվյալների սեփականության լիարժեք վերահսկողություն։
Մի՛ օգտագործեք, երբ
- Փոքր խանութ՝ ստանդարտ ապրանքներով և առանց ինտեգրացիաների․ Shopify-ի նման hosted պլատֆորմն ավելի արագ ու էժան է։
- Չունեք թիմ, որը launch-ից հետո կսպասարկի վճարումները, անվտանգության թարմացումները և ինտեգրացիաները։
- Բիզնես մոդելը դեռ ապացուցված չէ․ սկզբում վավերացրեք այն hosted խանութով։
Այլընտրանքներ
- Shopify
Hosted է, արագ գործարկվում է, ուժեղ էկոհամակարգ ունի․ custom տրամաբանությունն ապրում է app-երում և checkout extension-ներում՝ սահմանափակումներով։
- WooCommerce
WordPress plugin, էժան և ճկուն, բայց արագագործությունն ու կոդի որակը մեծապես կախված են plugin-ներից։
- Medusa / Saleor
Open-source headless commerce շարժիչներ․ լավ միջին տարբերակ, երբ ուզում եք կոդի վերահսկողություն՝ առանց զրոյից սկսելու։
- commercetools
Enterprise headless commerce API-ներ․ հզոր են, բայց գնով և թիմի պահանջներով նախատեսված են մեծ ընկերությունների համար։
Լավագույն պրակտիկաներ
- Գումարը պահեք որպես ամբողջ թիվ՝ արժույթի կոդի հետ․ երբեք float։
- Պատվերի տողերի վրա պահեք գների, զեղչերի, հարկերի և ապրանքների անունների snapshot-ը։
- Ամեն ընդհանուր գումար վերահաշվարկեք սերվերում․ client-ը ուղարկում է միայն ID-ներ և քանակներ։
- Պատվերի կարգավիճակները մոդելավորեք որպես բացահայտ state machine՝ history աղյուսակով։
- Մնացորդի փոփոխություններն արեք ատոմար և ազատեք ռեզերվները ձախողված կամ ժամկետանց վճարումների դեպքում։
- Email-ներն ու ինտեգրացիաներն ուղարկեք queue-ի job-երից՝ transaction-ի commit-ից հետո։
- Հյուրի զամբյուղը պահեք սերվերում և միավորեք login-ի ժամանակ։
Տարածված սխալներ
- Գների համար float սյունակներ օգտագործել և հաշվետվություններում ցենտերի տարբերություններ հայտնաբերել։
- Գները կարդալ պատվերի ընթացիկ ապրանքից՝ snapshot-ի փոխարեն, ինչի պատճառով հին հաշիվ-ապրանքագրերը փոխվում են։
- Վստահել բրաուզերից կամ հավելվածից ուղարկված ընդհանուր գումարին։
- Պատվերի կարգավիճակը փոխել ազատ update-ներով, ինչը հանգեցնում է անհնար վիճակների։
- Մնացորդը ստուգել միայն զամբյուղին ավելացնելիս, և ոչ նորից՝ checkout-ում։
Խորը վերլուծություն
07Idempotent պատվերի ստեղծում. երկակի սեղմումներից և retry-ներից պաշտպանություն
Հաճախորդը երկու անգամ սեղմում է «Պատվիրել», mobile app-ը timeout-ից հետո կրկնում է հարցումը, load balancer-ը նորից է ուղարկում request-ը։ Առանց պաշտպանության դրանցից յուրաքանչյուրը կարող է ստեղծել երկրորդ պատվեր և երկու անգամ ռեզերվել մնացորդը։ Լուծումը checkout key-ն է. client-ը checkout-ի էկրանը բացելիս գեներացնում է պատահական UUID և ուղարկում այն submit հարցման հետ։ Սերվերը պահում է այն պատվերի վրա unique constraint-ով։ Առաջին հարցումը ստեղծում է պատվերը, իսկ նույն key-ով ցանկացած կրկնություն վերադարձնում է արդեն գոյություն ունեցող պատվերը՝ նորը չստեղծելով։ Key-ը պահեք checkout-ի այդ փորձի ողջ ընթացքում և նորը գեներացրեք միայն այն ժամանակ, երբ զամբյուղը փոխվում է։ Սրան ավելացրեք կարճ lock յուրաքանչյուր զամբյուղի համար (Redis lock կամ advisory lock), որպեսզի երկու զուգահեռ հարցումներ միաժամանակ չանցնեն վալիդացիան, մինչև դրանցից որևէ մեկը կգրանցի պատվերը։
-- One order per checkout attempt, enforced by the database
ALTER TABLE orders ADD COLUMN checkout_key UUID;
CREATE UNIQUE INDEX orders_checkout_key_uq ON orders (checkout_key);
-- Inside the checkout transaction
INSERT INTO orders (checkout_key, customer_id, status, total_amount, currency)
VALUES ($1, $2, 'pending_payment', $3, $4)
ON CONFLICT (checkout_key) DO NOTHING
RETURNING id;
-- No row returned → a previous request already created it:
-- SELECT id FROM orders WHERE checkout_key = $1 and return that order.
08Զեղչերի բաշխում և կլորացում առանց մեկ ցենտ կորցնելու
Զամբյուղի մակարդակի զեղչը («5.00 զեղչ ամբողջ պատվերից») պետք է բաշխվի տողերի միջև, որովհետև հարկը, refund-ները և ERP export-ները աշխատում են տող առ տող։ Բաշխեք այն յուրաքանչյուր տողի արժեքին համամասնորեն, յուրաքանչյուր բաժին կլորացրեք ներքև, ապա մնացած ցենտերը տվեք ամենամեծ մնացորդ ունեցող տողերին։ Բաժինների գումարը միշտ ճշգրիտ հավասար է զեղչին, և հետագայում մեկ տողի մասնակի refund-ը վերադարձնում է հենց այն գումարը, որը գանձվել է այդ տողի համար։ Հաշվի առեք նաև արժույթի minor unit-ը ըստ ISO 4217-ի. արժույթների մեծ մասն ունի 2 տասնորդական նիշ, ճապոնական իենը՝ 0, իսկ քուվեյթական դինարը՝ 3։ Երբեք hard-code մի արեք «բաժանել 100-ի»։ Բազմարժույթ խանութների համար պատվերի վրա պահեք փոխարժեքի snapshot-ը և որոշեք՝ արդյոք միայն փոխարկում եք գները (ցուցադրման արժույթ), թե իրականում գանձում եք այդ արժույթով (settlement-ի արժույթ)։
// Split an integer amount across weights; parts always sum to the total
export function allocate(total: number, weights: number[]): number[] {
const sum = weights.reduce((a, b) => a + b, 0);
const raw = weights.map((w) => (total * w) / sum);
const parts = raw.map(Math.floor);
let rest = total - parts.reduce((a, b) => a + b, 0);
const order = raw
.map((r, i) => ({ i, frac: r - Math.floor(r) }))
.sort((a, b) => b.frac - a.frac);
for (const { i } of order) {
if (rest-- <= 0) break;
parts[i] += 1;
}
return parts;
}
allocate(500, [1999, 999, 2]); // [333, 167, 0] → sums to 500
09Read model-ներ կատալոգի արագ էջերի համար
Կատեգորիայի էջը, որն ամեն հարցման ժամանակ join է անում ապրանքները, տարբերակները, գները, մնացորդը, նկարները և ակցիաները, կատալոգի աճին զուգընթաց դառնում է դանդաղ։ Սովորական պատասխանը read model-ն է. denormalized աղյուսակ (կամ search index-ի document), որտեղ յուրաքանչյուր ապրանքի համար մեկ տող է՝ ճիշտ այն տվյալներով, որոնք պետք են ցուցակին. նվազագույն գինը, «առկա է» դրոշը, գլխավոր նկարը, ֆիլտրերի համար ատրիբուտների արժեքները, վարկանիշը։ Background job-երը թարմացնում են այն, երբ աղբյուր տվյալները փոխվում են, իսկ storefront-ը կարդում է միայն այս աղյուսակը։ Render արված ֆրագմենտները կամ API պատասխանները cache արեք tag-երով, որպեսզի գնի փոփոխությունը invalidate անի միայն համապատասխան ապրանքները։ Write model-ը (normalized աղյուսակներ, transaction-ներ) թողեք որպես ճշմարտության աղբյուր. read model-ը միշտ կարելի է նորից կառուցել դրանից։ Այս բաժանումը, որը երբեմն կոչվում է CQRS (command query responsibility segregation), թույլ է տալիս scale անել ընթերցումները՝ առանց checkout-ը բարդացնելու։
- Գնի, մնացորդի կամ բովանդակության փոփոխությունից հետո ապրանքի read տողը նորից կառուցեք queue-ի job-ում։
- Խորը էջերի համար ցուցակները paginate արեք keyset pagination-ով (WHERE id > last_id)։
- Ցուցակի յուրաքանչյուր ապրանքի համար երբեք live մի հաշվեք ակցիոն գները. badge-երը նախապես հաշվարկեք։
10Անվտանգության եզրային դեպքեր զամբյուղներում և պատվերներում
Զամբյուղներն ու պատվերները insecure direct object reference-ի (IDOR) դասական թիրախ են. /orders/1042-ը փոխում ես /orders/1043-ի և տեսնում ուրիշի պատվերը։ URL-ներում օգտագործեք չգուշակվող հանրային իդենտիֆիկատորներ (UUID-ներ կամ պատահական token-ներ) և միշտ սահմանափակեք query-ները ըստ սեփականատիրոջ։ Օգտատիրոջ login-ի պահին փոխեք guest զամբյուղի token-ը, որպեսզի մինչև login-ը որսացված token-ով հնարավոր չլինի կարդալ account-ի զամբյուղը։ Քանակները վալիդացրեք որպես դրական ամբողջ թվեր՝ վերին սահմանով. վատ կառուցված խանութներում բացասական քանակները իրական refund-ների են հանգեցրել։ Rate limit դրեք կուպոնի և checkout-ի endpoint-ների վրա՝ դանդաղեցնելու brute force-ը և card testing-ը, երբ հարձակվողները գողացված քարտերը փորձարկում են փոքր պատվերներով։ Եվ վերջապես, պատվերներում անձնական տվյալները պահեք նվազագույն ծավալով և պլանավորեք, թե որքան ժամանակ եք դրանք պահելու, որովհետև պատվերների աղյուսակները աննկատ դառնում են անձնական տվյալների ձեր ամենամեծ պահոցը։
Իրական իրավիճակներ
Flash sale-ի ժամանակ գլխավոր ապրանքը վաճառվում է մնացորդից ավելի
Խնդիր
Մեկժամյա ակցիայի ընթացքում վաճառվեց 300 միավոր, մինչդեռ գոյություն ուներ ընդամենը 200-ը։ Մնացորդը ստուգվում էր ապրանքը զամբյուղ ավելացնելիս և ևս մեկ անգամ application code-ում՝ պատվերը պահելուց անմիջապես առաջ։
Ինչպես լուծել
Ստուգումը և նվազեցումը երկու առանձին քայլեր էին, ուստի զուգահեռ checkout-ները բոլորը տեսնում էին, որ մնացորդ կա։ Դրանք փոխարինեք մեկ պայմանական update-ով՝ UPDATE stock_levels SET quantity = quantity - :qty WHERE variant_id = :id AND quantity >= :qty, պատվերի transaction-ի ներսում, և զրո affected rows-ը դիտարկեք որպես «վաճառված է»։ Ռեզերվեք պատվերը ստեղծելիս, ազատեք վճարման ձախողման դեպքում, և պլանավորված job-ով 15 րոպե անց չեղարկեք չվճարված պատվերները։ Հաջորդ ակցիայի համար ավելացրեք Redis counter որպես արագ նախնական ստուգում՝ բեռը թեթևացնելու համար, թողեք database-ի պաշտպանությունը որպես վերջնական իրավասու, և արշավից առաջ staging-ում զուգահեռ checkout-ներով load test անցկացրեք։
Հաճախորդները checkout-ում տեսնում են ավելի մեծ գումար, քան զամբյուղում
Խնդիր
Support-ը բողոքներ է ստանում, որ զամբյուղը ցույց է տալիս 42.00, բայց վճարման էկրանը պահանջում է 45.50։ Առանձին վերցրած՝ էկրաններից ոչ մեկում իրականում սխալ չկա։
Ինչպես լուծել
Սա գրեթե միշտ նշանակում է, որ գումարները հաշվարկվում են երկու տարբեր code path-երով. զամբյուղի էջը օգտագործում է պարզեցված հաշվարկ, իսկ checkout-ը այլ կերպ է կիրառում առաքման վճարները, ակցիաների կանոնները կամ հարկը։ Ամբողջ գնագոյացումը տեղափոխեք մեկ server-side pricing service, որը վերադարձնում է ամբողջական breakdown՝ տողեր, զեղչեր, առաքում, հարկ, ընդհանուր գումար, և երկու էկրաններն էլ թող render անեն այդ պատասխանը։ Երբ էկրանների միջև ինչ-որ բան օրինաչափորեն փոխվում է (առաքման վճարը՝ հասցեն մուտքագրելուց հետո, գնի թարմացում), վերադարձրեք հստակ հաղորդագրություն՝ նկարագրելով փոփոխությունը։ Ավելացրեք regression test, որը նույն զամբյուղը անցկացնում է cart endpoint-ով և checkout endpoint-ով և ստուգում, որ գումարները նույնն են։
Ընթրիքի ժամին առաքման ժամային միջակայքերը գերլցվում են
Խնդիր
Ռեստորանի մասնաճյուղը 15 րոպեում կարող է պատրաստել մոտ 20 պատվեր, բայց 19:00-ի միջակայքը պարբերաբար ստանում է 60 պատվեր։ Խոհանոցը հետ է մնում, իսկ ուշացած առաքումները հանգեցնում են refund-ների։
Ինչպես լուծել
Միջակայքերին պետք է capacity, ոչ միայն ժամեր։ Պահեք capacity յուրաքանչյուր մասնաճյուղի և միջակայքի համար (կարգավորելի՝ ըստ շաբաթվա օրվա) և միջակայքի ամրագրումը դիտարկեք ինչպես մնացորդը. booked_count-ի ատոմար պայմանական ավելացում, որը հաջողվում է միայն քանի դեռ այն capacity-ից փոքր է, պատվերի ստեղծման նույն transaction-ում։ Լցված միջակայքերը ցույց տվեք որպես անհասանելի և առաջարկեք մոտակա ազատը։ ASAP պատվերները պետք է capacity զբաղեցնեն պատրաստման ժամանակից հետո առաջին միջակայքից։ Մասնաճյուղի մենեջերներին տվեք switch՝ բեռնված պահին ASAP պատվերները դադարեցնելու կամ capacity-ն նվազեցնելու համար, և հաշվետվություն տրամադրեք միջակայքերի զբաղվածության մասին, որպեսզի բիզնեսը կարողանա պիկային ժամերին համապատասխան անձնակազմ ապահովել։
Արագ ուղեցույց
- Մնացորդի ատոմար նվազեցում
UPDATE stock_levels SET quantity = quantity - :qty WHERE variant_id = :id AND quantity >= :qty;- Տողի lock transaction-ի ներսում
SELECT quantity FROM stock_levels WHERE variant_id = :id FOR UPDATE;- Idempotent պատվերի insert (PostgreSQL)
INSERT INTO orders (checkout_key, ...) VALUES (...) ON CONFLICT (checkout_key) DO NOTHING RETURNING id;- Գումարի սյունակներ
amount BIGINT NOT NULL, currency CHAR(3) NOT NULL -- minor units + ISO 4217 code- Գումարի ֆորմատավորում ցուցադրման համար
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(1999 / 100) // $19.99- Guest զամբյուղի cookie
Set-Cookie: cart_token=<random>; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000; Path=/- Առաքման գոտու որոշում (PostGIS)
SELECT id FROM delivery_zones WHERE ST_Contains(area, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326));- Չվճարված պատվերների ժամկետի ավարտ (Laravel)
Schedule::command('orders:expire-pending')->everyFiveMinutes()->withoutOverlapping();
Իմ իրական նախագծերում
- Ռեստորանային ցանցի պատվերների պլատֆորմում ես կառուցեցի հյուրի զամբյուղներ՝ login-ի ժամանակ միավորումով և պատվերի կրկնությամբ, աշխարհագրական կոորդինատներից որոշվող առաքման գոտիներ և ժամային միջակայքերի շարժիչ՝ «հնարավորինս շուտ», պլանավորված առաքման և takeaway-ի համար՝ «business today»-ով և միջակայքերի կլորացմամբ։
- Կենդանիների ապրանքների պլատֆորմում կատալոգն ունի ներդրված կատեգորիաներ, բրենդներ, ատրիբուտների ընտանիքներ և տարբերակներ՝ տուփով և քաշով վաճառքի ռեժիմներով, իսկ գնագոյացումը մոդելավորված է value object-ներով․ checkout-ն օգտագործում է պատվերի lifecycle service և կարող է կատարել պատվերը պահեստից, մատակարարից կամ backorder-ով։
- Հաշվի, զամբյուղի և checkout-ի կրկնվող կոդը վերածեցի ներքին reusable package-ների, որոնք օգտագործվում են մոտ տասը հաճախորդի կայքում, և գիտակցաբար քարտի raw տվյալները դուրս թողեցի պահպանվող զամբյուղի store-ից։
Հարցեր և պատասխաններ
Q1Ինչո՞ւ գումարը երբեք չպետք է պահել float-ով։
Float-երը երկուական մոտարկումներ են, ուստի 0.1-ի նման արժեքները հնարավոր չէ ճշգրիտ ներկայացնել։ Գումարներում, զեղչերում և հարկի բաժանման մեջ փոքր սխալներ են առաջանում, և հազարավոր պատվերներից հետո հաշվապահությունն այլևս չի համընկնում վճարային պրովայդերի տվյալների հետ։ Ես պահում եմ ամբողջ թվեր՝ արժույթի ամենափոքր միավորով և ISO արժույթի կոդով, իսկ թվաբանության և ֆորմատավորման համար օգտագործում եմ money value object։ Կլորացումը կատարվում է մեկ տեղում՝ հստակ կանոնով, սովորաբար ըստ տողի։ Տվյալների բազայում և՛ integer, և՛ ֆիքսված DECIMAL սյունակն աշխատում են․ կարևորն այն է, որ հավելվածի կոդում երբեք float թվաբանություն չանեք։
Q2Ինչպե՞ս եք միավորում հյուրի զամբյուղը, երբ օգտատերը մուտք է գործում։
Հյուրի զամբյուղը սերվերային զամբյուղ է՝ կապված պատահական token-ի հետ։ Login-ի ժամանակ երկու զամբյուղն էլ բեռնում եմ transaction-ում, հյուրի յուրաքանչյուր տող տեղափոխում եմ օգտատիրոջ զամբյուղ, նույն տարբերակի քանակները գումարում եմ և սահմանափակում մնացորդով կամ մեկ պատվերի սահմանաչափով։ Հետո ջնջում եմ հյուրի զամբյուղը և մաքրում token-ը։ Միավորումից հետո ամբողջ զամբյուղը նորից վալիդացնում եմ և վերադարձնում նախազգուշացումները, օրինակ՝ ապրանքը, որն այլևս պահեստում չկա։ Սերվերում միավորելը վեբն ու հավելվածը համաձայնեցված է պահում և թույլ չի տալիս կորցնել ապրանքներ։
Q3Ինչպե՞ս եք կանխում, որ երկու հաճախորդ գնեն վերջին ապրանքը։
Մնացորդի փոփոխությունները պետք է ատոմար լինեն։ Transaction-ի ներսում ես կամ lock եմ անում մնացորդի տողը SELECT FOR UPDATE-ով, կամ աշխատեցնում եմ պայմանական update, որը նվազեցնում է միայն այն դեպքում, երբ բավարար քանակ կա, և ստուգում եմ ազդված տողերի քանակը։ Ռեզերվը կատարվում է պատվերը ստեղծելիս, ոչ թե զամբյուղին ապրանք ավելացնելիս։ Եթե վճարումը ձախողվում է կամ պատվերի ժամկետը լրանում է, job-ը ազատում է ռեզերվը։ Շատ պահանջված ապրանքների համար Redis counter-ը կամ queue-ն կարող են հերթականացնել հասանելիությունը, բայց տվյալների բազայի պաշտպանությունը մնում է վերջնական իշխանություն։
Q4Ինչո՞ւ պահել գների snapshot-ը պատվերի տողում։
Ապրանքները փոխվում են՝ գները բարձրանում են, անունները խմբագրվում են, ապրանքները ջնջվում են։ Պատվերը կոնկրետ պահին կատարվածի իրավական և ֆինանսական գրառում է, ուստի այն չպետք է կախված լինի ընթացիկ կատալոգից։ Յուրաքանչյուր պատվերի տողի վրա պատճենում եմ ապրանքի անունը, տարբերակի ատրիբուտները, միավորի գինը, զեղչը, հարկը և արժույթը։ Հաշիվ-ապրանքագրերը, վերադարձները, հաշվետվությունները և ERP export-ները այդ դեպքում կարդում են հենց պատվերից։ Սա նաև պարզեցնում է վերադարձները, որովհետև ճշգրիտ գիտեք, թե յուրաքանչյուր տողի համար ինչքան է գանձվել։
Q5Տեխնիկապես ինչպիսի՞ն է լավ checkout flow-ը։
Client-ը ուղարկում է զամբյուղի token-ը, fulfilment-ի ընտրությունը, հասցեն կամ մասնաճյուղը, ժամային միջակայքը և վճարման եղանակը։ Սերվերը նորից վալիդացնում է ամեն ինչ՝ հասանելիություն, մնացորդ, գներ, ակցիաներ, առաքման գոտի և միջակայք։ Եթե ամեն ինչ ճիշտ է, մեկ transaction-ում ստեղծում է պատվերը և ռեզերվում մնացորդը՝ «սպասում է վճարման» կարգավիճակով, ապա սկսում է վճարումը։ Պատվերը դառնում է վճարված միայն gateway-ից ստացված ստուգված հաստատումից հետո։ Email-ները, push-ը և ինտեգրացիաները գործարկվում են կարգավիճակի անցումներով՝ queue-ի job-երի միջոցով, իսկ ժամկետանց, վճարման սպասող պատվերները չեղարկվում են պլանավորված job-ով։
Q6Գները պետք է ներառե՞ն հարկը։
Կախված է շուկայից։ Եվրոպայի մեծ մասում, Հայաստանում և Ավստրալիայում սպառողական խանութները ցույց են տալիս հարկը ներառող գներ, և հարկն առանձնացվում է հաշիվ-ապրանքագրերի համար։ ԱՄՆ-ի խանութները սովորաբար ցույց են տալիս գները մինչև sales tax-ը, որը հաշվարկվում է առաքման հասցեից։ Գլխավորը մեկ խանութի համար մեկ կոնվենցիա ընտրելն է, յուրաքանչյուր պատվերի տողի վրա դրույքաչափն ու հարկի գումարը պահելը և կլորացման մեկ կանոն կիրառելը։ Բազմատարածաշրջանային բարդ հարկերի դեպքում Stripe Tax-ի կամ Avalara-ի նման հարկային ծառայությունը հաճախ ավելի էժան է, քան կանոններն ինքնուրույն սպասարկելը։
Սենիոր մակարդակի հարցեր
S1Ինչպե՞ս կդարձնեիք checkout-ը ամբողջությամբ անվտանգ retry-ների, կրկնակի submit-ների և զուգահեռ հարցումների նկատմամբ։
Ես կիրառում եմ պաշտպանության երեք շերտ։ Առաջին՝ client-ը ուղարկում է յուրաքանչյուր փորձի համար գեներացված checkout key, և orders աղյուսակի unique index-ի շնորհիվ կրկնված հարցումը վերադարձնում է արդեն ստեղծված պատվերը՝ նորը ստեղծելու փոխարեն։ Երկրորդ՝ յուրաքանչյուր զամբյուղի համար կարճ lock-ը հերթականացնում է զուգահեռ submit-ները, այնպես որ վալիդացիան և insert-ը կատարվում են մեկ անգամ։ Երրորդ՝ յուրաքանչյուր side effect idempotent է. մնացորդի ռեզերվն օգտագործում է պայմանական update-ներ, վճարման հարցումն ունի պատվերից ստացված idempotency key, իսկ email-ները կամ ERP export-ները կատարվում են queue-ի job-երից՝ պատվերով և անցումով key-ավորված։ Գինը մի փոքր լրացուցիչ state-ն ու մի քանի ավելի index-ներն են, բայց դա վերացնում է միջադեպերի մի ամբողջ դաս՝ կրկնակի պատվերներ և կրկնակի գանձումներ, որոնց ձեռքով մաքրումը շատ թանկ է։
S2Ե՞րբ կբաժանեիք կատալոգը, զամբյուղը և պատվերները առանձին service-ների։
Սկզբում՝ հազվադեպ։ Modular monolith-ը հստակ մոդուլներով՝ կատալոգ, pricing, զամբյուղ, պատվերներ, վճարումներ, և մեկ database-ով տալիս է transaction-ներ դրանց միջև, ինչից կախված է checkout-ը։ Ես կբաժանեի, երբ կա կոնկրետ ճնշում. կատալոգի ընթերցման բեռը պետք է scale արվի անկախ, տարբեր թիմեր պատասխանատու են տարբեր domain-ների համար և խանգարում են միմյանց release-ներին, կամ մոդուլն ունի հասանելիության էապես տարբեր պահանջներ։ Նույնիսկ այդ դեպքում ես նախ կառանձնացնեի ընթերցման ծանրաբեռնված մասերը, ինչպիսիք են search-ը և կատալոգի read model-ները, որոնք հանդուրժում են eventual consistency։ Պատվերներն ու վճարումները ամենաերկար են մնում միասին, որովհետև դրանց բաժանումը database transaction-ը փոխարինում է compensating քայլերի saga-ով, ավելի շատ ձախողման ռեժիմներով և ավելի բարդ debugging-ով։ Microservice-ները ավելի շատ լուծում են կազմակերպչական scale-ի խնդիրը, քան տեխնիկականը։
S3Ինչպե՞ս եք scale անում խանութը մեծ վաճառքի օրվա համար։
Ընթերցումներն ու գրառումները scale են արվում տարբեր կերպ։ Կատալոգի և ցուցակների traffic-ը հիմնականում ընթերցում է, ուստի ես այն մատուցում եմ read model-ներից, պատասխանները cache եմ անում CDN-ում կարճ TTL-ներով և tag-երով invalidation-ով, իսկ search-ը պահում եմ առանձին engine-ում։ Checkout-ը գրառումներով ծանրաբեռնված է և պետք է մնա ճշգրիտ, ուստի այն պահում եմ թեթև. request-ում ծանր աշխատանք չկա, side effect-ները queue-երում են, պատվերների և մնացորդի աղյուսակների index-ները ստուգված են, իսկ connection pooling-ը չափված է պիկի համար։ Թեժ ապրանքների համար database-ի մնացորդի պաշտպանությունից առաջ դրվում է արագ Redis նախնական ստուգում։ Ես load test եմ անում իրական checkout flow-ը, alert-ներ եմ կարգավորում queue-ի խորության և սխալների հաճախականության վրա, միջոցառման ընթացքում սառեցնում եմ deploy-ները և պատրաստում feature flag-եր, որպեսզի ծանրաբեռնվածության դեպքում անջատեմ ոչ էական ֆունկցիաները, օրինակ՝ recommendation-ները։
Բառարան
- SKU
- Stock-keeping unit. վաճառվող մեկ տարբերակի եզակի կոդ, որն օգտագործվում է մնացորդի, գնագոյացման և ինտեգրացիաների համար։
- Տարբերակ (variant)
- Ապրանքի կոնկրետ տարբերակ՝ ատրիբուտների որոշակի արժեքներով, օրինակ՝ կարմիր, M չափս։
- Minor unit
- Արժույթի ամենափոքր միավորը, օրինակ՝ ցենտը։ ISO 4217-ը սահմանում է, թե յուրաքանչյուր արժույթ քանի տասնորդական նիշ ունի։
- Գնի snapshot
- Անվան, գնի, զեղչի և հարկի պատճենը, որը գնման պահին պահվում է պատվերի տողի վրա։
- Ռեզերվ
- Կատարված պատվերի համար առանձնացված մնացորդ, որպեսզի մինչև առաքումը կամ չեղարկումը ոչ ոք այն չգնի։
- Checkout key
- Client-ի կողմից checkout-ի յուրաքանչյուր փորձի համար գեներացվող եզակի ID, որը retry-ների ժամանակ կանխում է կրկնակի պատվերները։
- Read model
- Տվյալների denormalized պատճեն՝ ձևավորված արագ ընթերցման համար և նորից կառուցվող աղբյուր աղյուսակներից։
- IDOR
- Insecure direct object reference. ուրիշի գրառմանը հասանելիություն՝ URL-ում կամ հարցման մեջ ID-ն փոխելով։
- Card testing
- Խարդախների կողմից գողացված քարտերով բազմաթիվ փոքր վճարումներ՝ պարզելու համար, թե որոնք են դեռ աշխատում։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։