บัตรสะสมแต้ม - ระบบระดับชั้น
ภาพรวม
กลไกจัดระดับลูกค้า (เช่น Bronze / Silver / Gold) ที่ตัดสินจาก ชุดกฎ (rule set) แทนที่จะเป็นตัวเลข threshold เพียงค่าเดียว แต่ละชั้นมีเงื่อนไขของตัวเอง วัดจาก 4 metric ภายในหน้าต่างเวลาที่กำหนดแยกได้ ต่อชั้น
ระบบนี้ใช้เฉพาะโหมด point ที่เปิด tier_enabled และเป็น logic ล้วนที่ไม่มี endpoint ของตัวเอง ถูกเรียก
จาก 3 จุด ได้แก่ ตอน earn (เพื่อเลื่อนชั้นทันที), ตอนโหลดหน้าบัตร (เพื่อแสดงชั้นและระยะที่เหลือ) และ
job รายคืนซึ่งอยู่ฝั่งอื่น
Business Flow
Metric ที่กฎวัดได้
spend— ยอดเงิน (บาท) ภายในหน้าต่างเวลาorders— จำนวน earn transaction ภายในหน้าต่างเวลาpoints— units ที่ได้รับภายในหน้าต่างเวลาmember_months— อายุสมาชิก โดยหน้าต่างเวลาไม่มีผลกับ metric นี้
การประเมิน (Qualifies / PickTierByRules)
ParseTierConditionsอ่านคอลัมน์ jsonbconditionsเป็นโครงสร้าง{join:"and"|"or", rules:[{metric,op,value}]}โดยค่าเริ่มต้นของjoinคือ"and"และค่าใดก็ตาม ที่ไม่ใช่"or"จะถูกบังคับเป็น"and"- กฎที่ parse ไม่สำเร็จถือว่าไม่ตรงกับใครเลย ไม่ใช่ตรงกับทุกคน เพราะการ fail open จะเลื่อนชั้น
ลูกค้าทั้งฐานเพียงเพราะแอดมินพิมพ์ผิด ซึ่งเป็นความผิดพลาดที่ถอนคืนไม่ได้เมื่อผู้คนเห็นชั้นใหม่ของตัวเอง
ไปแล้ว การ implement ทำโดย inject rule
metric:"__invalid__"ที่ไม่มีอะไร satisfy ได้ - rule set ที่ว่างเปล่าก็ไม่ตรงกับใคร เพราะชั้นที่ยังไม่ใส่เงื่อนไขคือชั้นที่ทำค้างไว้ครึ่งทาง การตีความว่า "ทุกคน" จะย้ายฐานลูกค้าทั้งหมดไปยัง rung ที่แอดมินกำลังพิมพ์อยู่
- metric ที่ไม่รู้จัก และ operator ที่ไม่รู้จัก (รองรับเฉพาะ
gte,lte,eq) ถือว่าไม่ผ่าน PickTierByRulesเดินบันได จากล่างขึ้นบนแล้วเก็บ match ตัวสุดท้าย จึงได้ rank สูงสุดที่ผ่าน ไม่ใช่ match ตัวแรก เพราะแอดมินเขียนกฎที่ทับซ้อนกันได้ และคนที่ผ่านทั้ง Silver และ Gold ต้องได้ Gold- ชั้น default (
is_default) ถูกข้ามในการเดินบันได กฎของมันไม่ถูกประเมินเลย และจะถูกคืนเฉพาะเมื่อ ไม่มีชั้นอื่นผ่าน ชั้น default จึงเป็น "พื้น" ที่แท้จริงซึ่งไม่สามารถชนะชั้นที่ลูกค้าหามาได้ ไม่ว่าจะตั้ง rank ไว้เท่าไรก็ตาม
การเรียงบันได (SortTiers)
เรียงด้วยค่า rank เท่านั้น ไม่เคยเรียงด้วย threshold เพราะแอดมินที่กำลังแก้บันไดอาจปล่อยให้สองชั้นมี
threshold เท่ากันชั่วขณะ และลำดับที่อิงตัวเลขจะสลับตำแหน่งตัวเองใต้มือของเขา
หน้าต่างเวลา (WindowFor)
ใช้ค่าของชั้นก่อนถ้าตั้งไว้ (window_months) ถ้าไม่มีจึงใช้ของโปรแกรม (tier_window_months) และถ้า
ไม่มีอีกจะใช้ค่าเริ่มต้น 3 เดือน เนื่องจากแต่ละชั้นวัดในหน้าต่างของตัวเอง maybePromote จึงเก็บสถิติ
แยกต่อชั้น ไม่ใช่เก็บครั้งเดียวต่อ account
การขึ้นและลงชั้น (ResolveFromEarned)
ฟังก์ชันนี้ถูกแยกออกจากการ "เลือกชั้น" เพื่อให้กฎการเคลื่อนไหวไม่เปลี่ยนไปเมื่อ threshold ถูกแทนที่ด้วย rule set กล่าวคือเปลี่ยนเฉพาะวิธีได้ชั้น ไม่ใช่สิ่งที่เกิดขึ้นเมื่อขึ้นหรือลง
| สถานะ | ผลลัพธ์ |
|---|---|
| ยังไม่มีชั้น และมี earned | TierInitial |
| ยังไม่มีชั้น และไม่มี earned | ไม่เขียนอะไร (reason ว่าง) |
| earned rank สูงกว่าปัจจุบัน | TierPromote |
| earned rank เท่ากับปัจจุบัน | ไม่เขียนอะไร เพื่อกัน tier_history เต็มไปด้วยแถวที่บอกว่า "อยู่ที่เดิม" |
earned rank ต่ำกว่าปัจจุบัน และ fallback เป็น step_down | ลง 1 ขั้น (byRank(rank-1)) ไม่ใช่กระโดดไปยังชั้นที่ earned เพื่อให้คนที่เงียบไปเดือนหนึ่งเสียเพียง 1 rung และได้คืนด้วยการกลับมาเพียงครั้งเดียว |
earned rank ต่ำกว่าปัจจุบัน และ fallback เป็น specific | ลงไปยังชั้นที่กำหนด ยกเว้นกรณีที่ชั้นนั้นมี rank ไม่ต่ำกว่าปัจจุบัน มิฉะนั้นคนที่นั่งอยู่บนชั้น fallback อยู่แล้วจะถูก "ลดชั้น" ไปที่เดิมทุกคืน |
ในบริบทของ earn (maybePromote)
รับเฉพาะผลลัพธ์ TierPromote และ TierInitial เท่านั้น ResolveFromEarned สามารถคืนการลดชั้นได้เมื่อ
หน้าต่างเวลาเลื่อน แต่การกระทำตามนั้นกลางธุรกรรมหน้าร้านเป็นสิ่งที่ห้ามทำ
nextSpendTarget
คำนวณตัวเลขเงินบาทของชั้นถัดไปสำหรับข้อความ "อีก ฿X" บนหน้าบัตร โดยหา rule ที่ metric เป็น spend
และ operator เป็น gte หรือ eq หากไม่พบจะได้ค่า 0 ซึ่งหมายถึงไม่แสดงตัวเลข เนื่องจากบันไดที่สร้าง
จากจำนวนออเดอร์หรืออายุสมาชิกไม่มีจำนวนเงินให้นับถอยหลัง การคิดตัวเลขขึ้นมาเองคือการใส่ค่าที่ไม่มี
ความหมายลงบนบัตรของลูกค้า
ไฟล์และฟังก์ชันหลัก
ฟีเจอร์นี้ไม่มี route ของตัวเอง
| ไฟล์ | ฟังก์ชัน |
|---|---|
internal/loyalty/tier.go | Tier, SortTiers, NextTier, byRank, ResolveFromEarned และค่าคงที่ FallbackStepDown, FallbackSpecific, TierPromote, TierDemote, TierInitial |
internal/loyalty/tier_rules.go | TierRule, TierConditions, AccountStats, ParseTierConditions, statFor, matchRule, Qualifies, PickTierByRules, WindowFor, nextSpendTarget และ metric MetricSpend/MetricOrders/MetricPoints/MetricMemberMonths |
internal/loyalty/service.go | (*Service).maybePromote ซึ่งเป็นจุดเรียกในเส้นทาง earn |
internal/loyalty/handler.go | บล็อก tier ใน GetCard (GetTierByID, ListTiersByLineOa, NextTier, SpendInWindow) |
internal/loyalty/repository.go | ListTiers, ListTiersByLineOa, GetTierByID, StatsInWindow, SpendInWindow, SetTier |
internal/loyalty/view.go | NewTierView (สี, สีตัวอักษร, รูปพื้นหลังต่อชั้น) |
จุดเชื่อมต่อกับ Service อื่น
- ตารางฐานข้อมูล
loyalty.tier(jsonbconditions,rank,window_months,is_default,status,color,text_color,bg_image_path),loyalty.tier_history,loyalty.account(tier_id),loyalty.transactionซึ่งเป็นแหล่งของสถิติ และloyalty.program(tier_enabled,tier_window_months,tier_fallback) - ถูกเรียกจาก loyalty-earn และ loyalty-card และเป็นด่าน
ของ loyalty-reward-redeem ผ่าน
min_tier_rank - Tier ไม่ได้ version ตามโปรแกรม แต่ผูกกับ OA เหตุผลอธิบายไว้ที่ loyalty-card
- job ประเมินชั้นใหม่และลดชั้นรายคืนอยู่นอก service นี้ (ฝั่ง worker/CMS) แต่ต้องใช้กฎชุดเดียวกัน
- ตรงกับ client-web feature:
loyalty-card