案例 25:会员等级成长值权益升降级
本案例对应 USE-CASES 场景 25「会员等级成长值权益升降级(诉求 26)」,属于电商零售领域,面向会员运营系统的周期性等级评定场景,自动完成本期成长值累计与退款率折减、目标等级评定、升降级判定、等级折扣装配,并对违规套取权益的会员执行一期冻结。
电商会员体系设五个等级:S1 注册、S2 银卡、S3 金卡、S4 白金、S5 黑金。每个评定周期系统采集会员账户与消费行为要素:会员号、姓名、期初等级、期初累计成长值、本期消费金额(元)、本期成交订单数、本期退款率、等级有效期剩余天数、违规套取权益次数,据此评定期末等级并装配可执行权益。
本期成长值按消费行为累计:每消费 1 元计 1 分,每成交一单额外计 20 分,合计为原始成长值;退款率达到 30% 及以上的只按 50% 计入,15%-30%(含 15%)的按 80% 计入,其余全额计入。期末累计成长值达到 50000 评定 S5、20000 评定 S4、8000 评定 S3、2000 评定 S2,其余为 S1。将期初等级与评定等级的序位(S1=1 至 S5=5)比较,得到 UPGRADE 升级、DOWNGRADE 降级或 MAINTAIN 保级。
等级权益随评定结果发放:S5 黑金全场 85 折并配专属客户经理、全场免运费,S4 白金全场 9 折、全场免运费,S3 金卡全场 94 折,S2 银卡全场 97 折,S1 无等级折扣。等级有效期剩余 30 天以内且无违规记录的会员触发临期保级提醒,提示保持消费即可保级。
违规套利单独管控:本期违规套取权益达到 2 次及以上的,本期新增成长值清零、权益冻结标记置位,等级强制 MAINTAIN、折扣按 1.0 执行,且只装配冻结说明与(如临期无违规条件不成立则不发)保级提醒,防止通过虚假交易冲级。决策结果携带期末成长值、本期新增、期初/目标等级、变动类型、折扣、冻结与提醒标记,以及逐条权益说明,直接下发到会员中心与营销触达渠道。
- 成长值与等级阈值随运营节奏调整:升级线 2000/8000/20000/50000、每单 20 分、退款折减档与各等级折扣经常随运营策略修订,硬编码进会员系统后每次大促前都要赶发版。
- 退款折减是百分比整数运算:50%、80% 折减若引入浮点可能产生小数成长值,成长值体系要求整数口径,还要保证折减顺序与边界(15%、30%)正确。
- 升降级要同时比较等级编码:S1-S5 是字符串编码,不能直接比较,须先转序位再判定,且折扣必须取评定后等级而非期初等级。
- 违规冻结须贯穿多个环节:成长值清零、强制保级、折扣归一、权益文案替换必须一致执行,单点漏改就会出现「冻结仍升级」或「冻结仍打折」的资损事故。
- 权益文案要随等级与变动类型组合装配:升级、降级、保级三种说明叠加五档权益文案再加临期提醒,组合分支多,散落在调用方难以维护。
- 会员系统高并发与轻量嵌入要求:等级评定跑在周期批处理与实时接口上,不接受重型规则引擎的额外依赖与启动开销。
本案例沿用六件套骨架:入参模型 + 决策结果模型 + 规则接口 + XML 规则脚本 + Service 编排 + 6 组真实业务形态测试。
| 层次 | 职责 | 对应文件 |
|---|---|---|
| 领域模型 | 会员账户与本期消费要素、等级评定与权益决策结果 | MemberProfile.java、MemberLoyaltyDecision.java |
| 规则集与 XML | 成长值累计与退款折减、等级阈值、等级序位、折扣系数、临期提醒 | MemberLoyaltyRules.java、member-loyalty.xml |
| 编排服务 | 违规冻结清零、成长值汇总、升降级比较、权益与提醒文案装配 | MemberLoyaltyService.java |
| 测试 | 6 组会员形态(临界黑金/银升金/白金降级/金卡退款折减保级/违规冻结/临期提醒) | MemberLoyaltyServiceTest.java |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 口径 | 分值/输出 |
|---|---|---|
periodEarn |
原始成长值 = 消费金额(1 元 1 分)+ 订单数 ×20;退款率 ≥30% 按 50% 计、≥15% 按 80% 计,否则全额 | 整数成长值 |
targetLevel |
累计成长值 ≥50000 S5、≥20000 S4、≥8000 S3、≥2000 S2,否则 S1 | 五个等级 |
levelRank |
等级序位:S5=5、S4=4、S3=3、S2=2,其余(S1)=1 | 1-5 |
levelDiscount |
等级折扣:S5 0.85、S4 0.90、S3 0.94、S2 0.97,其余 1.0 | 折扣系数 |
renewalReminder |
等级剩余有效期 ≤30 天且违规次数为 0 时提醒,否则不提醒 | boolean |
代码实现(完整可运行)
Section titled “代码实现(完整可运行)”将下列文件按源码包结构放入 JQuick-Java 工程的测试目录:两个模型放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口与测试类放入 src/test/java/com/github/paohaijiao/demo/,Service 放入 src/test/java/com/github/paohaijiao/demo/service/,XML 脚本放入 src/test/resources/demo/,包名保持源码原样(com.github.paohaijiao.demo / .model / .service)。放入后执行 mvn test -Dtest=MemberLoyaltyServiceTest 即可运行全部 6 个用例。
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; }}在工程根目录执行:
mvn test -Dtest=MemberLoyaltyServiceTest测试覆盖形态
Section titled “测试覆盖形态”| 测试方法 | 构造的业务形态 | 关键断言/期望决策 |
|---|---|---|
shouldUpgradeToBlackGoldAtThreshold |
S4 期初 47000、本期消费 3000 元、0 单、无退款 | 新增 3000,期末恰 50000;S5、UPGRADE、折扣 0.85;含「黑金权益:全场 85 折、专属客户经理、全场免运费」 |
shouldUpgradeSilverToGold |
S2 期初 6000、消费 2500 元、5 单 | 原始 2500+5×20=2600 全额计入,期末 8600;S3、UPGRADE、折扣 0.94 |
shouldDowngradePlatinumWithoutSpending |
S4 期初 15000、零消费零订单、退款率 40 | 新增 0;评定 S3、DOWNGRADE;含「成长值未达当前等级保级线,等级由 S4 调整为 S3」 |
shouldMaintainGoldWithRefundRateDeduction |
S3 期初 8000、消费 1000 元、5 单、退款率 20 | 原始 1100 按 80% 计 880,期末 8880;S3、MAINTAIN、折扣 0.94 |
shouldFreezeBenefitsForRepeatedViolation |
S3 期初 19000、消费 5000 元、20 单但违规 2 次 | 新增清零为 0、期末仍 19000;强制 S3、MAINTAIN;frozen=true、折扣 1.0;首条权益含「违规套取权益行为,新增成长值清零」 |
shouldRemindRenewalWithinThirtyDays |
两个子场景:S2 剩余 15 天到期;剩余 200 天 | 前者 renewalReminder=true 且含「距到期仅剩 15 天,保持消费即可保级」;后者 false 不提醒 |
- 等级与积分口径全量外置 XML:四级升级线、每单 20 分、退款率 15%/30% 两档折减、五档折扣、30 天临期线全部脚本化,运营调档只需改 XML 重建代理,会员系统大促前免发版。
- 百分比折减用整数运算保口径:脚本以
raw * 50 / 100、raw * 80 / 100完成折减,先乘后除全程整数,成长值不产生小数,边界 15%/30% 按「就高不就低」落入对应折减档。 - 等级编码数值化后再比较:
levelRank把 S1-S5 映射为 1-5,Service 用序位差一次性得出 UPGRADE/DOWNGRADE/MAINTAIN,折扣统一取评定后等级的levelDiscount,避免字符串比较与口径分叉。 - 违规冻结在 Java 侧一处裁决、全链路一致:冻结时新增成长值直接清零、变动类型强制 MAINTAIN、折扣归一 1.0,权益装配提前返回只保留冻结说明,杜绝「冻结仍升级、冻结仍打折」的资损。
- 布尔条件正向返回适配脚本语法:
renewalReminder以嵌套 if 正向返回 true/false,不使用引擎不支持的取反表达式;临期提醒由 Service 在权益装配后追加,组合文案互不干扰。 @Param按名绑定 + ASM 动态代理、Java 8 无重型依赖:5 个规则方法共用一个代理,可嵌入高并发会员评定链路,评定结果与权益文案逐条下发、可回溯。