之前五阳挑战使用 1行代码实现一个工作流审批功能,# 挑战仅用一行代码实现请假审批流程
今天五阳计划挑战用最少的代码实现商品购买、退款的电商交易系统。
业界调研
开源平台上有很多在线商城系统,功能很全,然而实际业务场景非常复杂和多样化,开源的在线商城系统很难完全匹配实际业务,很多项目
- 功能堆砌,大部分功能用不上,需要大量裁剪;
- 逻辑差异点较多,需要大量修改;
- 功能之间耦合,难以独立替换某个功能。
电商交易系统和工作流系统都是类似的------------业务足够复杂、足够灵活,开源系统不能把业务功能写死,替用户做决定,而是要提供足够的灵活性和扩展性。
这里五阳采用 memberclub 交易引擎框架,基于它提供的 sdk 构建商品购买退款等交易功能。
memberclub 是开源免费的交易引擎,通过扩展点引擎和流程引擎定义复杂的交易流程, 以SDK方式对外提供通用的交易能力,能让开发者像搭积木方式,从0到1,快速构建一个新的电商交易系统!
基本功能
- 支持在线课程购买和退款。
- 单商品下单和多商品下单,支持加购多份,支持购买限额即1个商品最多购买N次。
- 支持整单退。
先展示下效果
效果
初始化项目
- Git clone gitee.com/juejinwuyan...
- 创建 memberclub.plugin.lesson 子工程
pom中dependency 只需要依赖 memberclub.sdk
js
<dependencies>
<dependency>
<groupId>com.memberclub</groupId>
<artifactId>memberclub.sdk</artifactId>
<version>${baseversion}</version>
</dependency>
</dependencies>
接下来看具体功能实现
购买域实现
MemberClub提供了 PurchaseExtension 扩展点,除必选流程外,开发者可以按需选择交易子流程。不需要的流程注释掉即可。
如下是五阳实现的在线课程购买功能扩展点,一共定义了 3个流程,分别为下单流程、未支付取消流程、退款取消流程。每个流程配置了多个子节点,节点实现交易能力,如提单检查、提单锁、下单凭证记录、限额检查、库存检查、用户标签记录等。
在线课程没有库存限制,因此我们删除了库存相关的节点,那么下单流程就不会再检查库存。
js
@ExtensionProvider(desc = "Lesson 购买提单扩展点", bizScenes = {
@Route(bizType = BizTypeEnum.LESSON, scenes = {SceneEnum.HOMEPAGE_SUBMIT_SCENE})
})
public class LessonPurchaseExtension implements PurchaseExtension {
private static FlowChain<PurchaseSubmitContext> submitChain = null;
private static FlowChain<AfterSaleApplyContext> purchaseReverseChain = null;
private static FlowChain<PurchaseCancelContext> purchaseCancelFlowChain = null;
@Autowired
private MemberOrderDomainService memberOrderDomainService;
@PostConstruct
public void init() {
submitChain = FlowChain.newChain(PurchaseSubmitContext.class)
.addNode(PurchaseSubmitLockFlow.class)
.addNode(SkuInfoInitalSubmitFlow.class)
.addNode(PurchaseSubmitCmdValidateFlow.class)
.addNode(PurchaseUserQuotaFlow.class) //检查限额
//.addNode(PurchaseValidateInventoryFlow.class) //检查库存
.addNode(MemberOrderSubmitFlow.class) // 会员提单
//.addNode(PurchaseMarkNewMemberFlow.class) //新会员标记
//.addNode(PurchaseOperateInventoryFlow.class) //扣减库存
.addNode(CommonOrderSubmitFlow.class) //订单系统提单
;
purchaseReverseChain = FlowChain.newChain(AfterSaleApplyContext.class)
//.addNode(PurchaseReverseNewMemberFlow.class)
//.addNode(PurchaseReverseInventoryFlow.class)
.addNode(PurchaseReverseMemberQuotaFlow.class)
//
;
purchaseCancelFlowChain = FlowChain.newChain(PurchaseCancelContext.class)
.addNode(PurchaseCancelLockFlow.class)
.addNode(PurchaseCancelOrderFlow.class)
//.addNode(PurchaseCancelNewMemberFlow.class)
.addNode(PurchaseCancelQuotaFlow.class)
//.addNode(PurchaseCancelInventoryFlow.class)
;
}
@Override
public void submit(PurchaseSubmitContext context) {
submitChain.execute(context);
}
@Override
public void reverse(AfterSaleApplyContext context) {
purchaseReverseChain.execute(context);
}
@Override
public void cancel(PurchaseCancelContext context) {
MemberOrderDO memberOrder = memberOrderDomainService.
getMemberOrderDO(context.getCmd().getUserId(), context.getCmd().getTradeId());
context.setMemberOrder(memberOrder);
purchaseCancelFlowChain.execute(context);
}
}
订单模型
订单模型分为主单和子单,一笔订单可包括多个商品,每个商品支持多份数购买。考虑到结算、履约、售后的需要,在商家、商品等维度上,系统通常会将主单拆分为多笔子订单,拆分成多笔子订单以后,履约、售后等流程均基于子订单操作,如履约时分商品分别进行履约,售后时支持指定商品退款。
js
CREATE TABLE IF NOT EXISTS member_order (
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '表自增主键',
biz_type INT(11) NOT NULL COMMENT '产品线',
user_id BIGINT(20) NOT NULL COMMENT 'userId',
order_system_type INT(11) NULL COMMENT '订单系统类型',
order_id VARCHAR(128) NULL COMMENT '订单 id',
trade_id VARCHAR(128) NOT NULL COMMENT '交易 id',
renew_type INT(11) NOT NULL COMMENT '续费类型 0 无续费,1 用户续费 2 系统自动续费',
act_price_fen INT(11) NULL COMMENT '实付金额',
origin_price_fen INT(11) NOT NULL COMMENT '原价金额',
sale_price_fen INT(11) NOT NULL COMMENT '原价金额',
source INT(11) NOT NULL COMMENT '开通来源',
status INT(11) NOT NULL COMMENT '主状态',
perform_status INT(11) NOT NULL COMMENT '履约状态',
extra TEXT NOT NULL COMMENT '扩展属性',
stime BIGINT(20) NULL DEFAULT '0' COMMENT '开始时间',
etime BIGINT(20) NULL DEFAULT '0' COMMENT '截止时间',
utime BIGINT(20) NOT NULL DEFAULT '0' COMMENT '更新时间',
ctime BIGINT(20) NOT NULL DEFAULT '0' COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uniq_member_order (user_id, trade_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ;
js
CREATE TABLE IF NOT EXISTS member_sub_order (
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '表自增主键',
biz_type INT(11) NOT NULL COMMENT '产品线',
user_id BIGINT(20) NOT NULL COMMENT 'userId',
order_system_type INT(11) NULL COMMENT '订单系统类型',
order_id VARCHAR(128) NULL COMMENT '订单 id',
trade_id VARCHAR(128) NOT NULL COMMENT '交易 id',
sub_trade_id BIGINT(20) NOT NULL COMMENT '子单交易 id',
sku_id BIGINT(20) NOT NULL COMMENT 'skuId',
act_price_fen INT(11) NULL COMMENT '实付金额',
origin_price_fen INT(11) NULL COMMENT '原价金额',
sale_price_fen INT(11) NOT NULL COMMENT '原价金额',
buy_count INT(11) NOT NULL COMMENT '购买数量',
status INT(11) NOT NULL COMMENT '主状态',
perform_status INT(11) NOT NULL COMMENT '履约状态',
extra TEXT NOT NULL COMMENT '扩展属性',
stime BIGINT(20) NULL DEFAULT '0' COMMENT '开始时间',
etime BIGINT(20) NULL DEFAULT '0' COMMENT '截止时间',
utime BIGINT(20) NOT NULL DEFAULT '0' COMMENT '更新时间',
ctime BIGINT(20) NOT NULL DEFAULT '0' COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uniq_sub_order (user_id, trade_id, sku_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ;
履约域实现
memberclub在履约域 提供了两个必选扩展点,PerformSeparateOrderExtension 和 PerformExecuteExtension 分别为履约拆合单扩展点、履约执行扩展点。
拆合单扩展点的主要功能是将把订单拆分成多份,构建履约计划,分别进行履约。例如年卡会员需要拆分成12期分别进行履约,如续费购买等场景下,拆单模块在构建履约计划时,不会立即履约首期权益,而是等到生效时间以后才履约。
js
@ExtensionProvider(desc = "在线课程履约拆单扩展点实现类", bizScenes = {
@Route(bizType = BizTypeEnum.LESSON, scenes = {SceneEnum.SCENE_MONTH_CARD}),
})
public class LessonPerformSeparateOrderExtension implements PerformSeparateOrderExtension {
FlowChain<PerformContext> performSeparateOrderChain = null;
@Autowired
private FlowChainService flowChainService;
@Autowired
private PerformDomainService performDomainService;
@PostConstruct
public void run() throws Exception {
performSeparateOrderChain = FlowChain.newChain(flowChainService, PerformContext.class)
.addNode(InitialSkuPerformContextsFlow.class)
.addNode(MutilBuyCountClonePerformItemFlow.class)
//如果年卡周期是自然月,则可以在此处根据当前期数计算每期的天数
.addNode(CalculateImmediatePerformItemPeriodFlow.class)//计算立即履约项 时间周期
.addNode(CalculateOrderPeriodFlow.class)//计算订单整体有效期
.addNode(PerformContextExtraInfoBuildFlow.class)// 构建扩展属性
;
}
@Override
public void separateOrder(PerformContext context) {
flowChainService.execute(performSeparateOrderChain, context);
}
/**
* 取履约项最大时间和最小时间 作为整笔订单的有效期。
*
* @param context
*/
@Override
public void buildTimeRange(PerformContext context) {
performDomainService.buildTimeRangeOnPerformBaseMaxMin(context);
}
}
履约执行
履约拆单模块会构建好履约计划,履约执行模块会执行两个动作 1)发放立即履约的权益 2)针对未来履约的权益,创建延迟履约任务。
js
@ExtensionProvider(desc = "在线课程 执行履约扩展点", bizScenes = {
@Route(bizType = BizTypeEnum.LESSON, scenes = {SceneEnum.SCENE_MONTH_CARD})//在线课程多商品
})
public class LessonPerformExecuteExtension implements PerformExecuteExtension {
private FlowChain<PerformContext> flowChain;
private FlowChain<PerformContext> subFlowChain;
@Autowired
private FlowChainService flowChainService;
@PostConstruct
public void init() {
subFlowChain = FlowChain.newChain(flowChainService, PerformContext.class)
.addNode(SingleSubOrderPerformFlow.class)
.addNodeWithSubNodes(ImmediatePerformFlow.class, PerformItemContext.class,
// 构建 MemberPerformItem, 发放权益
ImmutableList.of(MemberPerformItemFlow.class, PerformItemGrantFlow.class));
flowChain = FlowChain.newChain(flowChainService, PerformContext.class)
.addNode(MemberResourcesLockFlow.class)
.addNode(MemberOrderOnPerformSuccessFlow.class)
.addNode(MemberPerformMessageFlow.class)
//.addNode(MemberExpireRefundTaskCreatedFlow.class)
.addNodeWithSubNodes(MutilSubOrderPerformFlow.class, subFlowChain)
;
}
@Override
public void execute(PerformContext context) {
flowChainService.execute(flowChain, context);
}
}
课程权益
在线课程购买后,用户的课程表中会包含购买的课程,点击课程能进入到课程详情页面,在课程开始后用户可以进入到课堂。这部分业务功能不属于交易履约能力,因此交易系统需要和课程系统定义好系统边界。
在交易履约阶段 交易系统只需要发放一张上课凭证,同时将购买信息同步到课程系统即可。因此这部分业务实现时非常简单,包含一个资格类权益,记录用户可上课的权益资格,同时通知课程系统即可。
MemberClub 提供了会员资格权益的履约能力,因此我们只需要新增一个 课程权益,将其权益类型声明为 资格权益,然后在 会员资格权益扩展点生效范围上,增加课程权益。
js
public enum RightTypeEnum {
COUPON(1, RightUsedType.ASSET, "会员立减券"),
DISCOUNT_COUPON(2, RightUsedType.ASSET, "会员折扣券"),
MEMBERSHIP(3, RightUsedType.SHIP, "会员身份"),
FREE_FREIGHT_COUPON(4, RightUsedType.ASSET, "免运费券"),
MEMBER_DISCOUNT_PRICE(5, RightUsedType.SHIP, "会员价"),
LESSON(6, RightUsedType.SHIP, "在线课程"),
LESSON_COUPON(7, RightUsedType.ASSET, "购课立减券"),
;
资格类权益扩展点
资格类权益扩展点生效范围 新增 SceneEnum.RIGHT_TYPE_SCENE_LESSONSHIP
js
@ExtensionProvider(desc = "会员资格类权益发放扩展点 默认实现", bizScenes = {
@Route(bizType = BizTypeEnum.DEMO_MEMBER, scenes = {
SceneEnum.RIGHT_TYPE_SCENE_MEMBERSHIP, SceneEnum.RIGHT_TYPE_SCENE_MEMBER_DISCOUNT_PRICE
}),
@Route(bizType = BizTypeEnum.LESSON, scenes = {
SceneEnum.RIGHT_TYPE_SCENE_LESSONSHIP
}),
})
public class DefaultShipGrantExtension implements MemberRightsGrantExtension {
@Autowired
private MemberShipDomainService memberShipDomainService;
@Autowired
private MemberShipDataObjectFactory memberShipDataObjectFactory;
@Override
public ItemGroupGrantResult grant(PerformItemContext context, List<MemberPerformItemDO> items) {
ItemGroupGrantResult result = new ItemGroupGrantResult();
Map<String, ItemGrantResult> grantMap = Maps.newHashMap();
result.setGrantMap(grantMap);
for (MemberPerformItemDO item : items) {
MemberShipDO memberShipDO = memberShipDataObjectFactory.buildMemberShipDO(context, item);
memberShipDomainService.grant(memberShipDO);
ItemGrantResult itemGrantResult = new ItemGrantResult();
itemGrantResult.setBatchCode(memberShipDO.getGrantCode());
grantMap.put(memberShipDO.getGrantCode(), itemGrantResult);
}
return result;
}
}
售后部分
售后模块要具备整单退的能力。MemberClub提供了 AfterSaleApplyExtension 扩展点,在这个扩展点中,我们复用了系统提供的 售后资源锁定流程、售后预览流程、生单流程、逆向履约流程、消单流程、退款流程等。
js
@ExtensionProvider(desc = "在线课程售后受理扩展点", bizScenes = {
@Route(bizType = BizTypeEnum.LESSON, scenes = {SceneEnum.SCENE_AFTERSALE_MONTH_CARD})
})
public class LessonAfterSaleApplyExtension implements {
FlowChain<AfterSaleApplyContext> applyFlowChain = null;
FlowChain<AfterSaleApplyContext> checkFlowChain = null;
FlowChain<AfterSaleApplyContext> doApplyFlowChain = null;
@Autowired
private FlowChainService flowChainService;
@PostConstruct
public void init() {
applyFlowChain = FlowChain.newChain(flowChainService, AfterSaleApplyContext.class)
.addNode(AftersaleApplyLockFlow.class) //加锁
.addNode(AftersaleApplyPreviewFlow.class) //售后预览
.addNode(AfterSalePlanDigestCheckFlow.class) //校验售后计划摘要
.addNode(AftersaleGenerateOrderFlow.class) //生成售后单
.addNode(AftersaleDoApplyFlow.class)
;
doApplyFlowChain = FlowChain.newChain(flowChainService, AfterSaleApplyContext.class)
.addNode(AftersaleOrderDomainFlow.class)
.addNode(MemberOrderRefundSuccessFlow.class) //售后成功后, 更新主单子单的状态为成功
.addNode(AftersaleAsyncRollbackFlow.class) // 失败异步回滚
.addNode(AftersaleReversePerformFlow.class) //逆向履约
.addNode(AftersaleReversePurchaseFlow.class) //逆向取消订单
.addNode(AftersaleRefundOrderFlow.class) //退款
//.addNode()
;
}
@Override
public void apply(AfterSaleApplyContext context) {
flowChainService.execute(applyFlowChain, context);
}
@Override
public void doApply(AfterSaleApplyContext context) {
flowChainService.execute(doApplyFlowChain, context);
}
@Override
public void customBuildAftersaleOrder(AfterSaleApplyContext context, AftersaleOrderDO aftersaleOrderDO) {
}
}
效果
总结
基于memberclub 框架,我们实现了 4个扩展点,就轻松实现了 课程购买和课程退款功能。 一共代码不超过200行,绝大部分还是拷贝的。
效率还是很高的。框架地址在:gitee.com/juejinwuyan...