跳转到内容

案例 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
规则方法 口径 分值/输出
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

将下列文件按源码包结构放入 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 个用例。

package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 会员账户与本期消费行为要素
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public 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;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 会员等级评定与权益决策结果
*/
@Data
public 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);
}
}
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);
}
<?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>
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("注册会员:暂无等级折扣,多消费可快速升级");
}
}
}
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;
}
}

在工程根目录执行:

Terminal window
mvn test -Dtest=MemberLoyaltyServiceTest
测试方法 构造的业务形态 关键断言/期望决策
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 个规则方法共用一个代理,可嵌入高并发会员评定链路,评定结果与权益文案逐条下发、可回溯。