Case 25: Member loyalty tier upgrades
This case corresponds to USE-CASES scenario 25, “Member tier growth value and benefit upgrade/downgrade (requirement 26)”. It belongs to the e-commerce retail domain, targeting the periodic tier assessment of a member operations system. It automatically accumulates the current period’s growth value with refund-rate deductions, assesses the target tier, determines upgrades and downgrades, assembles tier discounts, and applies a one-period benefit freeze to members who abuse benefits.
Industry scenario
Section titled “Industry scenario”The e-commerce membership system has five tiers: S1 registered, S2 silver, S3 gold, S4 platinum, S5 black gold. Each assessment period, the system collects member account and consumption behavior elements: member ID, name, starting tier, accumulated growth value at period start, current-period spend (yuan), current-period completed orders, current-period refund rate, remaining days of tier validity, and number of benefit-abuse violations. Based on these, it assesses the end-of-period tier and assembles executable benefits.
Current-period growth value accumulates from consumption behavior: 1 point per yuan spent, plus 20 extra points per completed order, summed as the raw growth value; a refund rate of 30% or above counts only 50%, 15%-30% (inclusive of 15%) counts 80%, and anything else counts in full. An accumulated growth value of 50000 assesses S5, 20000 assesses S4, 8000 assesses S3, 2000 assesses S2, and anything else is S1. Comparing the ordinal positions of the starting tier and the assessed tier (S1=1 through S5=5) yields UPGRADE, DOWNGRADE, or MAINTAIN.
Tier benefits are issued with the assessment result: S5 black gold gets 15% off storewide, a dedicated account manager, and free shipping storewide; S4 platinum gets 10% off storewide and free shipping storewide; S3 gold gets 6% off storewide; S2 silver gets 3% off storewide; S1 has no tier discount. Members with 30 or fewer days of tier validity remaining and no violation record trigger an expiring-tier retention reminder, telling them that keeping up spending preserves the tier.
Violation abuse is controlled separately: members with 2 or more benefit-abuse violations in the period have their newly earned growth value zeroed, the benefit-frozen flag set, the tier forced to MAINTAIN, and the discount set to 1.0; only the freeze explanation and (if the expiring-no-violation condition does not hold, nothing is sent) the retention reminder are assembled, preventing tier rushing through fake transactions. The decision result carries the end-of-period growth value, the newly earned value, the starting/target tiers, the change type, the discount, the freeze and reminder flags, and itemized benefit explanations, delivered directly to the member center and marketing outreach channels.
Business pain points
Section titled “Business pain points”- Growth value and tier thresholds change with operations cadence: the upgrade lines 2000/8000/20000/50000, the 20 points per order, the refund deduction tiers, and the per-tier discounts are frequently revised with operations strategy; hard-coding them into the member system forces a rushed release before every major promotion.
- Refund deduction is integer percentage arithmetic: the 50% and 80% deductions could produce fractional growth values if floats were introduced; the growth value system requires integer semantics, and the deduction order and boundaries (15%, 30%) must be correct.
- Upgrade/downgrade requires comparing tier codes: S1-S5 are string codes that cannot be compared directly; they must be converted to ordinal positions first, and the discount must come from the assessed tier, not the starting tier.
- Violation freeze must be consistent across multiple steps: growth value zeroing, forced tier retention, discount normalization, and benefit copy replacement must execute consistently; a single missed spot causes loss incidents like “frozen but still upgraded” or “frozen but still discounted”.
- Benefit copy must be assembled from tier and change-type combinations: three explanation types (upgrade, downgrade, retention) stacked on five tiers of benefit copy plus an expiry reminder create many combination branches that are hard to maintain when scattered across callers.
- High concurrency and lightweight embedding requirements for member systems: tier assessment runs on periodic batch jobs and real-time interfaces, and cannot accept the extra dependencies and startup overhead of a heavy rule engine.
Solution architecture
Section titled “Solution architecture”This case follows the six-piece skeleton: input model + decision result model + rule interface + XML rule script + Service orchestration + 6 groups of tests based on real business scenarios.
| Layer | Responsibility | Files |
|---|---|---|
| Domain model | Member account and current-period consumption elements, tier assessment and benefit decision result | MemberProfile.java, MemberLoyaltyDecision.java |
| Rule set and XML | Growth value accumulation and refund deduction, tier thresholds, tier ordinals, discount factors, expiry reminder | MemberLoyaltyRules.java, member-loyalty.xml |
| Orchestration service | Violation freeze and zeroing, growth value aggregation, upgrade/downgrade comparison, benefit and reminder copy assembly | MemberLoyaltyService.java |
| Tests | 6 member scenarios (boundary black gold / silver-to-gold / platinum downgrade / gold retention with refund deduction / violation freeze / expiry reminder) | MemberLoyaltyServiceTest.java |
Rule method breakdown
Section titled “Rule method breakdown”| Rule method | Rule | Score / output |
|---|---|---|
periodEarn |
Raw growth value = spend (1 point per yuan) + orders x20; refund rate >=30% counts 50%, >=15% counts 80%, otherwise full | Integer growth value |
targetLevel |
Accumulated growth value >=50000 S5, >=20000 S4, >=8000 S3, >=2000 S2, otherwise S1 | Five tiers |
levelRank |
Tier ordinal: S5=5, S4=4, S3=3, S2=2, otherwise (S1) 1 | 1-5 |
levelDiscount |
Tier discount: S5 0.85, S4 0.90, S3 0.94, S2 0.97, otherwise 1.0 | Discount factor |
renewalReminder |
Reminds when remaining validity ≤30 days and violation count is 0, otherwise no reminder | boolean |
Code implementation (complete & runnable)
Section titled “Code implementation (complete & runnable)”Place the following files into the test directory of a JQuick-Java project following the source package structure: the two models go into src/test/java/com/github/paohaijiao/demo/model/, the rule interface and test class into src/test/java/com/github/paohaijiao/demo/, the Service into src/test/java/com/github/paohaijiao/demo/service/, and the XML script into src/test/resources/demo/, keeping the original package names (com.github.paohaijiao.demo / .model / .service). Then run mvn test -Dtest=MemberLoyaltyServiceTest to execute all 6 test cases.
MemberProfile.java
Section titled “MemberProfile.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 会员账户与本期消费行为要素 */@Data@NoArgsConstructor@AllArgsConstructorpublic class MemberProfile {
/** * 会员号 */ private String memberId;
/** * 会员姓名 */ private String memberName;
/** * 当前等级:S1 注册 / S2 银卡 / S3 金卡 / S4 白金 / S5 黑金 */ private String currentLevel;
/** * 期初累计成长值 */ private int growthValue;
/** * 本期消费金额(元) */ private int periodSpendYuan;
/** * 本期成交订单数 */ private int periodOrders;
/** * 本期退款率(整数百分比) */ private int refundRate;
/** * 等级有效期剩余天数 */ private int daysToExpire;
/** * 违规套取权益次数 */ private int violationCount;}MemberLoyaltyDecision.java
Section titled “MemberLoyaltyDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 会员等级评定与权益决策结果 */@Datapublic class MemberLoyaltyDecision {
/** * 会员号 */ private String memberId;
/** * 期末累计成长值 */ private int newGrowthValue;
/** * 本期新增成长值 */ private int periodEarned;
/** * 期初等级 */ private String currentLevel;
/** * 评定后等级 */ private String targetLevel;
/** * 等级变动:UPGRADE 升级 / DOWNGRADE 降级 / MAINTAIN 保级 */ private String changeType;
/** * 评定等级对应折扣(权益冻结时为 1.0) */ private double discount;
/** * 是否触发临期保级提醒 */ private boolean renewalReminder;
/** * 权益是否因违规被冻结 */ private boolean benefitFrozen;
/** * 等级权益与评定说明 */ private List<String> benefits = new ArrayList<String>();
public void addBenefit(String benefit) { this.benefits.add(benefit); }}MemberLoyaltyRules.java
Section titled “MemberLoyaltyRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 25:会员成长值累计与等级评定规则 */public interface MemberLoyaltyRules {
/** * 本期可获成长值:每消费 1 元计 1 分,每单额外 20 分;高退款率折减 */ int periodEarn(@Param("spendYuan") int spendYuan, @Param("orders") int orders, @Param("refundRate") int refundRate);
/** * 按累计成长值评定等级 */ String targetLevel(@Param("growthValue") int growthValue);
/** * 等级序位(S1=1 ... S5=5) */ int levelRank(@Param("level") String level);
/** * 等级折扣系数 */ double levelDiscount(@Param("level") String level);
/** * 临期 30 天内且无违规,推送保级提醒 */ boolean renewalReminder(@Param("daysToExpire") int daysToExpire, @Param("violationCount") int violationCount);}member-loyalty.xml
Section titled “member-loyalty.xml”<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN" "classpath:paohaijiao/dtd/Jquick-java.dtd"><javas namespace="com.github.paohaijiao.demo.MemberLoyaltyRules">
<java name="periodEarn" returnClass="int"> <![CDATA[ int def periodEarn(int:spendYuan, int:orders, int:refundRate) { int raw = spendYuan + orders * 20; if (refundRate >= 30) { return raw * 50 / 100; } else if (refundRate >= 15) { return raw * 80 / 100; } else { return raw; } } ]]> </java>
<java name="targetLevel" returnClass="java.lang.String"> <![CDATA[ str def targetLevel(int:growthValue) { if (growthValue >= 50000) { return "S5"; } else if (growthValue >= 20000) { return "S4"; } else if (growthValue >= 8000) { return "S3"; } else if (growthValue >= 2000) { return "S2"; } else { return "S1"; } } ]]> </java>
<java name="levelRank" returnClass="int"> <![CDATA[ int def levelRank(str:level) { if (level == "S5") { return 5; } else if (level == "S4") { return 4; } else if (level == "S3") { return 3; } else if (level == "S2") { return 2; } else { return 1; } } ]]> </java>
<java name="levelDiscount" returnClass="double"> <![CDATA[ double def levelDiscount(str:level) { if (level == "S5") { return 0.85; } else if (level == "S4") { return 0.90; } else if (level == "S3") { return 0.94; } else if (level == "S2") { return 0.97; } else { return 1.0; } } ]]> </java>
<java name="renewalReminder" returnClass="boolean"> <![CDATA[ boolean def renewalReminder(int:daysToExpire, int:violationCount) { if (daysToExpire <= 30) { if (violationCount == 0) { return true; } else { return false; } } else { return false; } } ]]> </java>
</javas>MemberLoyaltyService.java
Section titled “MemberLoyaltyService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.MemberLoyaltyRules;import com.github.paohaijiao.demo.model.MemberLoyaltyDecision;import com.github.paohaijiao.demo.model.MemberProfile;
/** * 场景 25:会员成长值累计、等级升降级与权益装配 */public class MemberLoyaltyService {
private final MemberLoyaltyRules rules;
public MemberLoyaltyService(MemberLoyaltyRules rules) { this.rules = rules; }
public MemberLoyaltyDecision evaluate(MemberProfile member) { MemberLoyaltyDecision decision = new MemberLoyaltyDecision(); decision.setMemberId(member.getMemberId()); decision.setCurrentLevel(member.getCurrentLevel());
int earned = rules.periodEarn(member.getPeriodSpendYuan(), member.getPeriodOrders(), member.getRefundRate());
boolean frozen = member.getViolationCount() >= 2; if (frozen) { earned = 0; } decision.setPeriodEarned(earned); decision.setBenefitFrozen(frozen);
int newGrowth = member.getGrowthValue() + earned; decision.setNewGrowthValue(newGrowth);
String target = rules.targetLevel(newGrowth); decision.setTargetLevel(target);
int currentRank = rules.levelRank(member.getCurrentLevel()); int targetRank = rules.levelRank(target);
double discount = rules.levelDiscount(target); if (frozen) { decision.setChangeType("MAINTAIN"); decision.setDiscount(1.0); } else if (targetRank > currentRank) { decision.setChangeType("UPGRADE"); decision.setDiscount(discount); } else if (targetRank < currentRank) { decision.setChangeType("DOWNGRADE"); decision.setDiscount(discount); } else { decision.setChangeType("MAINTAIN"); decision.setDiscount(discount); }
assembleBenefits(decision, member, target, currentRank, targetRank, frozen);
boolean reminder = rules.renewalReminder(member.getDaysToExpire(), member.getViolationCount()); decision.setRenewalReminder(reminder); if (reminder) { decision.addBenefit("等级距到期仅剩 " + member.getDaysToExpire() + " 天,保持消费即可保级,敬请关注续级提醒"); }
return decision; }
private void assembleBenefits(MemberLoyaltyDecision decision, MemberProfile member, String target, int currentRank, int targetRank, boolean frozen) { if (frozen) { decision.addBenefit("本期存在 " + member.getViolationCount() + " 次违规套取权益行为,新增成长值清零,升降级与折扣权益冻结一期"); return; } if ("UPGRADE".equals(decision.getChangeType())) { decision.addBenefit("成长值达到 " + decision.getNewGrowthValue() + " 分,等级由 " + member.getCurrentLevel() + " 升级为 " + target); } else if ("DOWNGRADE".equals(decision.getChangeType())) { decision.addBenefit("成长值未达当前等级保级线,等级由 " + member.getCurrentLevel() + " 调整为 " + target); } else { decision.addBenefit("成长值 " + decision.getNewGrowthValue() + " 分,维持 " + target + " 等级"); } if ("S5".equals(target)) { decision.addBenefit("黑金权益:全场 85 折、专属客户经理、全场免运费"); } else if ("S4".equals(target)) { decision.addBenefit("白金权益:全场 9 折、全场免运费"); } else if ("S3".equals(target)) { decision.addBenefit("金卡权益:全场 94 折"); } else if ("S2".equals(target)) { decision.addBenefit("银卡权益:全场 97 折"); } else { decision.addBenefit("注册会员:暂无等级折扣,多消费可快速升级"); } }}MemberLoyaltyServiceTest.java
Section titled “MemberLoyaltyServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.MemberLoyaltyDecision;import com.github.paohaijiao.demo.model.MemberProfile;import com.github.paohaijiao.demo.service.MemberLoyaltyService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 25:会员成长值与等级升降级 * <p> * 覆盖 6 组会员形态:临界升级黑金、银卡升金卡、白金未保级降级、 * 金卡保级(高退款折减成长值)、违规冻结权益、临期保级提醒。 */public class MemberLoyaltyServiceTest {
private MemberLoyaltyService service;
@Before public void setUp() { MemberLoyaltyRules rules = JQuickJava.create() .createApi(MemberLoyaltyRules.class, "demo/member-loyalty.xml"); this.service = new MemberLoyaltyService(rules); }
/** * 成长值恰好 50000 → 白金升黑金,85 折 */ @Test public void shouldUpgradeToBlackGoldAtThreshold() { MemberProfile member = base("M001", "S4", 47000); member.setPeriodSpendYuan(3000); member.setPeriodOrders(0);
MemberLoyaltyDecision d = service.evaluate(member);
Assert.assertEquals(50000, d.getNewGrowthValue()); Assert.assertEquals("S5", d.getTargetLevel()); Assert.assertEquals("UPGRADE", d.getChangeType()); Assert.assertEquals(0.85, d.getDiscount(), 0.0001); Assert.assertTrue(d.getBenefits().contains("黑金权益:全场 85 折、专属客户经理、全场免运费")); }
/** * 银卡会员大额消费 → 升级金卡,94 折 */ @Test public void shouldUpgradeSilverToGold() { MemberProfile member = base("M002", "S2", 6000); member.setPeriodSpendYuan(2500); member.setPeriodOrders(5);
MemberLoyaltyDecision d = service.evaluate(member);
Assert.assertEquals(2600, d.getPeriodEarned()); Assert.assertEquals(8600, d.getNewGrowthValue()); Assert.assertEquals("S3", d.getTargetLevel()); Assert.assertEquals("UPGRADE", d.getChangeType()); Assert.assertEquals(0.94, d.getDiscount(), 0.0001); }
/** * 白金会员本期零消费且高退款率 → 成长值 15000 降至金卡 */ @Test public void shouldDowngradePlatinumWithoutSpending() { MemberProfile member = base("M003", "S4", 15000); member.setPeriodSpendYuan(0); member.setPeriodOrders(0); member.setRefundRate(40);
MemberLoyaltyDecision d = service.evaluate(member);
Assert.assertEquals(0, d.getPeriodEarned()); Assert.assertEquals("S3", d.getTargetLevel()); Assert.assertEquals("DOWNGRADE", d.getChangeType()); Assert.assertTrue(d.getBenefits().contains( "成长值未达当前等级保级线,等级由 S4 调整为 S3")); }
/** * 金卡会员保级,退款率 20% 档成长值按 80% 计入 */ @Test public void shouldMaintainGoldWithRefundRateDeduction() { MemberProfile member = base("M004", "S3", 8000); member.setPeriodSpendYuan(1000); member.setPeriodOrders(5); member.setRefundRate(20);
MemberLoyaltyDecision d = service.evaluate(member);
Assert.assertEquals(880, d.getPeriodEarned()); Assert.assertEquals(8880, d.getNewGrowthValue()); Assert.assertEquals("S3", d.getTargetLevel()); Assert.assertEquals("MAINTAIN", d.getChangeType()); Assert.assertEquals(0.94, d.getDiscount(), 0.0001); }
/** * 两期违规套取权益 → 本期成长值清零,折扣冻结,升降级冻结 */ @Test public void shouldFreezeBenefitsForRepeatedViolation() { MemberProfile member = base("M005", "S3", 19000); member.setPeriodSpendYuan(5000); member.setPeriodOrders(20); member.setViolationCount(2);
MemberLoyaltyDecision d = service.evaluate(member);
Assert.assertEquals(0, d.getPeriodEarned()); Assert.assertEquals(19000, d.getNewGrowthValue()); Assert.assertEquals("S3", d.getTargetLevel()); Assert.assertEquals("MAINTAIN", d.getChangeType()); Assert.assertTrue(d.isBenefitFrozen()); Assert.assertEquals(1.0, d.getDiscount(), 0.0001); Assert.assertTrue(d.getBenefits().get(0).contains("违规套取权益行为,新增成长值清零")); }
/** * 等级 30 天内到期触发保级提醒,远期不提醒 */ @Test public void shouldRemindRenewalWithinThirtyDays() { MemberProfile expiring = base("M006", "S2", 1900); expiring.setPeriodSpendYuan(100); expiring.setDaysToExpire(15); MemberLoyaltyDecision d1 = service.evaluate(expiring); Assert.assertTrue(d1.isRenewalReminder()); Assert.assertTrue(d1.getBenefits().contains("等级距到期仅剩 15 天,保持消费即可保级,敬请关注续级提醒"));
MemberProfile safe = base("M007", "S2", 1900); safe.setPeriodSpendYuan(100); safe.setDaysToExpire(200); MemberLoyaltyDecision d2 = service.evaluate(safe); Assert.assertFalse(d2.isRenewalReminder()); }
private MemberProfile base(String id, String level, int growth) { MemberProfile m = new MemberProfile(); m.setMemberId(id); m.setMemberName("测试会员"); m.setCurrentLevel(level); m.setGrowthValue(growth); m.setPeriodSpendYuan(0); m.setPeriodOrders(0); m.setRefundRate(0); m.setDaysToExpire(100); m.setViolationCount(0); return m; }}Run & verified results
Section titled “Run & verified results”Run from the project root:
mvn test -Dtest=MemberLoyaltyServiceTestTest coverage
Section titled “Test coverage”| Test method | Business scenario constructed | Key assertions / expected decision |
|---|---|---|
shouldUpgradeToBlackGoldAtThreshold |
S4 with 47000 at period start, 3000 yuan spent this period, 0 orders, no refunds | Earned 3000, end-of-period exactly 50000; S5, UPGRADE, discount 0.85; contains “黑金权益:全场 85 折、专属客户经理、全场免运费” |
shouldUpgradeSilverToGold |
S2 with 6000 at period start, 2500 yuan spent, 5 orders | Raw 2500+5x20=2600 counted in full, end-of-period 8600; S3, UPGRADE, discount 0.94 |
shouldDowngradePlatinumWithoutSpending |
S4 with 15000 at period start, zero spend and zero orders, refund rate 40 | Earned 0; assessed S3, DOWNGRADE; contains “成长值未达当前等级保级线,等级由 S4 调整为 S3” |
shouldMaintainGoldWithRefundRateDeduction |
S3 with 8000 at period start, 1000 yuan spent, 5 orders, refund rate 20 | Raw 1100 counted at 80% as 880, end-of-period 8880; S3, MAINTAIN, discount 0.94 |
shouldFreezeBenefitsForRepeatedViolation |
S3 with 19000 at period start, 5000 yuan spent, 20 orders, but 2 violations | Earned zeroed to 0, end-of-period stays 19000; forced S3, MAINTAIN; frozen=true, discount 1.0; first benefit contains “违规套取权益行为,新增成长值清零” |
shouldRemindRenewalWithinThirtyDays |
Two sub-scenarios: S2 with 15 days to expiry; 200 days to expiry | The former has renewalReminder=true and contains “距到期仅剩 15 天,保持消费即可保级”; the latter is false with no reminder |
Technical highlights
Section titled “Technical highlights”- Tier and points rules fully externalized to XML: the four upgrade lines, the 20 points per order, the 15%/30% two-tier refund deductions, the five discount tiers, and the 30-day expiry line are all scripted; operations retuning only requires editing the XML and rebuilding the proxy, with no release of the member system before major promotions.
- Percentage deduction in integer arithmetic preserves semantics: the script performs deduction with
raw * 50 / 100andraw * 80 / 100, multiplying before dividing so it stays integer throughout, the growth value never becomes fractional, and the 15%/30% boundaries fall into the correct deduction tier on a “higher rate wins” basis. - Tier codes numericized before comparison:
levelRankmaps S1-S5 to 1-5, the Service derives UPGRADE/DOWNGRADE/MAINTAIN from the ordinal difference in one step, and the discount uniformly takeslevelDiscountof the assessed tier, avoiding string comparison and rule divergence. - Violation freeze decided once on the Java side and applied consistently end to end: when frozen, newly earned growth value is zeroed directly, the change type is forced to MAINTAIN, the discount is normalized to 1.0, and benefit assembly returns early keeping only the freeze explanation — eliminating losses like “frozen but still upgraded, frozen but still discounted”.
- Positive boolean returns adapted to script syntax:
renewalReminderreturns true/false via nested positive ifs, without negation expressions the engine does not support; the expiry reminder is appended by the Service after benefit assembly, so the combined copy never interferes. @Paramname binding + ASM dynamic proxy, Java 8 with no heavy dependencies: the 5 rule methods share a single proxy, embeddable into high-concurrency member assessment pipelines, with assessment results and benefit copy delivered item by item and fully traceable.