md_files/项目/拼团设计模式.md

29 KiB
Raw Blame History

设计模式

单例模式

懒汉

public class LazySingleton {
    private static volatile LazySingleton instance;
    
    private LazySingleton() {}
    
    public static LazySingleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (LazySingleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

第一次检查防止重复实例化、以及进行synchronized同步块。

第二次检查防止有多个线程同时通过第一次检查然后依次进入同步块后创建N个实例。

volatile防止指令重排序instance = new LazySingleton(); 正确顺序是:

1.分配内存

2.调用构造函数,初始化对象

3.把引用赋给 instance

饿汉

public class EagerSingleton {
    // 类加载时就初始化实例
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    
    // 私有构造函数
    private EagerSingleton() {
        // 防止反射创建实例
        if (INSTANCE != null) {
            throw new IllegalStateException("Singleton already initialized");
        }
    }
    
    // 全局访问点
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
    
    // 防止反序列化破坏单例
    private Object readResolve() {
        return INSTANCE;
    }
}

工厂模式

简单工厂

// 产品接口
interface Product {
    void use();
}

// 具体产品A
class ConcreteProductA implements Product {
    @Override
    public void use() {
        System.out.println("使用产品A");
    }
}

// 具体产品B
class ConcreteProductB implements Product {
    @Override
    public void use() {
        System.out.println("使用产品B");
    }
}

class SimpleFactory {
    // 根据参数创建不同的产品
    public static Product createProduct(String type) {
        switch (type) {
            case "A":
                return new ConcreteProductA();
            case "B":
                return new ConcreteProductB();
            default:
                throw new IllegalArgumentException("未知产品类型");
        }
    }
}

public class Client {
    public static void main(String[] args) {
        // 通过工厂创建产品
        Product productA = SimpleFactory.createProduct("A");
        productA.use();  // 输出: 使用产品A
        
        Product productB = SimpleFactory.createProduct("B");
        productB.use();  // 输出: 使用产品B
    }
}

缺点:添加新产品需要修改工厂类(违反开闭原则)

抽象工厂

抽象工厂模式是一种创建型设计模式,它提供一个接口用于创建相关或依赖对象的家族,而不需要明确指定具体类。

// 抽象产品接口
interface Button {
    void render();
}

interface Checkbox {
    void render();
}

// 具体产品实现 - Windows 风格
class WindowsButton implements Button {
    @Override
    public void render() {
        System.out.println("渲染一个 Windows 风格的按钮");
    }
}

class WindowsCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("渲染一个 Windows 风格的复选框");
    }
}

// 具体产品实现 - MacOS 风格
class MacOSButton implements Button {
    @Override
    public void render() {
        System.out.println("渲染一个 MacOS 风格的按钮");
    }
}

class MacOSCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("渲染一个 MacOS 风格的复选框");
    }
}

// 抽象工厂接口
interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// 具体工厂实现 - Windows
class WindowsFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new WindowsButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new WindowsCheckbox();
    }
}

// 具体工厂实现 - MacOS
class MacOSFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new MacOSButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new MacOSCheckbox();
    }
}

// 客户端代码
public class Application {
    private Button button;
    private Checkbox checkbox;
    
    public Application(GUIFactory factory) {
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }
    
    public void render() {
        button.render();
        checkbox.render();
    }
    
    public static void main(String[] args) {
        // 根据配置或环境选择工厂
        GUIFactory factory;
        
        String osName = System.getProperty("os.name").toLowerCase();
        if (osName.contains("win")) {
            factory = new WindowsFactory();
        } else {
            factory = new MacOSFactory();
        }
        
        Application app = new Application(factory);
        app.render();
    }
}

模板方法

核心思想

在抽象父类中定义算法骨架(固定执行顺序),把某些可变步骤留给子类重写;调用方只用模板方法,保证流程一致。

如果仅仅是把重复的方法抽取成公共函数,不叫模板方法!模板方法要设计算法骨架!!!

 Client  ───▶  AbstractClass
                ├─ templateMethod()  ←—— 固定流程
                │    step1()
                │    step2()         ←—— 抽象,可变
                │    step3()
                └─ hookMethod()      ←—— 可选覆盖
                       ▲
                       │ extends
            ┌──────────┴──────────┐
            │ ConcreteClassA/B…   │

示例:

// 1. 抽象模板
public abstract class AbstractDialog {

    // 模板方法:固定调用顺序,设为 final 防止子类改流程
    public final void show() {
        initLayout();
        bindEvent();
        beforeDisplay();       // 钩子,可选
        display();
        afterDisplay();        // 钩子,可选
    }

    // 具体公共步骤
    private void initLayout() {
        System.out.println("加载通用布局文件");
    }

    // 需要子类实现的抽象步骤
    protected abstract void bindEvent();

    // 钩子方法,默认空实现
    protected void beforeDisplay() {}
    protected void afterDisplay() {}

    private void display() {
        System.out.println("弹出对话框");
    }
}

// 2. 子类:登录对话框
public class LoginDialog extends AbstractDialog {
    @Override
    protected void bindEvent() {
        System.out.println("绑定登录按钮事件");
    }
    @Override
    protected void afterDisplay() {
        System.out.println("focus 到用户名输入框");
    }
}

// 3. 调用
public class Demo {
    public static void main(String[] args) {
        AbstractDialog dialog = new LoginDialog();
        dialog.show();
        /* 输出:
           加载通用布局文件
           绑定登录按钮事件
           弹出对话框
           focus 到用户名输入框
         */
    }
}

要点

  • 复用公共流程initLayout()display() 写一次即可。
  • 限制流程顺序show() 定为 final,防止子类乱改步骤。
  • 钩子方法:子类可选择性覆盖(如 beforeDisplay)。

策略模式

核心思想

将可以互换的算法或行为抽象为独立的策略类,运行时由**上下文类Context**选择合适的策略对象去执行。调用方Client只依赖统一的接口不关心具体实现。

┌───────────────┐
│   Client      │
└─────▲─────────┘
      │ has-a
┌─────┴─────────┐                 implements
│  Context      │────────────┐  ┌──────────────┐
│ (使用者)      │ strategy    └─▶│ Strategy A   │
└───────────────┘                ├──────────────┤
                                 │ Strategy B   │
                                 └──────────────┘
// 策略接口
public interface PaymentStrategy {
    void pay(int amount);
}

// 策略A微信支付
@Service("wechat")
public class WechatPay implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("使用微信支付 " + amount + " 元");
    }
}

// 策略B支付宝支付
@Service("alipay")
public class Alipay implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("使用支付宝支付 " + amount + " 元");
    }
}

// 上下文类
public class PaymentContext {
    private PaymentStrategy strategy;

    public PaymentContext(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void execute(int amount) {
        strategy.pay(amount);
    }
}

// 调用方
public class Main {
    public static void main(String[] args) {
        PaymentContext ctx = new PaymentContext(new WechatPay());
        ctx.execute(100);

        ctx = new PaymentContext(new Alipay());
        ctx.execute(200);
    }
}

下面有更优雅的策略选择方式!

Spring集合自动注入

在策略、工厂、插件等模式中,经常需要维护**“策略名 → 策略对象”**的映射。Spring 可以通过 Map<String, 接口类型> 一次性注入所有实现类。

@Resource       
private Map<String, IDiscountCalculateService> discountCalculateServiceMap;

字段类型Map<String, IDiscountCalculateService>

  • key—— Bean 的名字
    • 默认是类名首字母小写 (mjCalculateService)
    • 或者你在实现类上显式写的 @Service("MJ")
  • value —— 那个实现类对应的实例
  • Spring 机制
    1. 启动时扫描所有实现 IDiscountCalculateService 的 Bean。
    2. 把它们按 “BeanName → Bean 实例” 的映射注入到这张 Map 里。
    3. 你一次性就拿到了“策略字典”。

示例:

// 上下文类:自动注入所有策略 Bean
@Component
@RequiredArgsConstructor
public class PaymentContext {

    // key 为 Bean 名(如 "wechat"、"alipay"value 为策略实例
    private final Map<String, PaymentStrategy> paymentStrategyMap;

    public void pay(String strategyKey, int amount) {
        PaymentStrategy strategy = paymentStrategyMap.get(strategyKey);
        if (strategy == null) {
            throw new IllegalArgumentException("无匹配支付方式: " + strategyKey);
        }
        strategy.pay(amount);
    }
}

// 调用方示例
@Component
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentContext paymentContext;

    public void process() {
        paymentContext.pay("wechat", 100);  // 输出:使用微信支付 100 元
        paymentContext.pay("alipay", 200);  // 输出:使用支付宝支付 200 元
    }
}

模板方法+策略模式

本项目的价格试算同时用了策略模式 + 模板方法模式:

策略模式Strategy IDiscountCalculateService 是策略接口;ZKCalculateServiceZJCalculateService ...是可替换的折扣策略@Service("ZK") / @Service("ZJ") 作为选择键)。外部可以根据活动配置里的类型码选哪个实现来算价——这就是“运行时可切换算法”。

模板方法模式Template Method AbstractDiscountCalculateService#calculate(...)共同流程固定下来(先进行人群校验 → 计算优惠后价格),并把“真正的计算”这一步延迟到子类通过 doCalculate(...) 实现。

责任链

应用场景:日志系统、审批流程、权限校验——任何需要将请求按阶段传递、并由某一环节决定是否继续或终止处理的地方,都非常适合责链模式。

image-20250808215823342

典型的责任链模式要点:

  • 解耦请求发送者和处理者:调用者只持有链头,不关心中间环节。
  • 动态组装:通过 appendNext 可以灵活地增加、删除或重排链上的节点。
  • 可扩展:新增处理逻辑只需继承 AbstractLogicLink 并实现 apply,不用改动已有代码。

单实例链

可以理解成“单向、单链表式的链条”:每个节点只知道自己的下一个节点(next),链头只有一个入口。 你可以在启动或运行时动态组装head.appendNext(a).appendNext(b).appendNext(c);

T / D / R 是啥?

  • T:请求的静态入参(本次请求的主要数据)。
  • D动态上下文(链路里各节点共享、可读写的状态容器,比如日志收集、校验中间结果)。
  • R:最终返回结果类型。

1接口定义ILogicChainArmory<T, D, R> 提供添加节点方法和获取节点

// 定义了“链条组装”的最小能力:能拿到下一个节点、也能把下一个节点接上去
public interface ILogicChainArmory<T, D, R> {

    // 获取当前节点的“下一个”处理者
    ILogicLink<T, D, R> next();

    // 把新的处理者挂到当前节点后面,并返回它(方便链式 append
    ILogicLink<T, D, R> appendNext(ILogicLink<T, D, R> next);
}

2ILogicLink<T, D, R> 继承自 ILogicChainArmory<T, D, R>,并额外声明了核心方法 apply

// 真正的“处理节点”接口:在具备链条组装能力的基础上,还要能“处理请求”
public interface ILogicLink<T, D, R> extends ILogicChainArmory<T, D, R> {
    R apply(T requestParameter, D dynamicContext) throws Exception;
}

3抽象基类AbstractLogicLink,提供了责任链节点的通用骨架,(保存 next、实现 appendNext/next()、以及一个便捷的 protected next(...),这样具体的节点类就不用重复这些代码,真正的业务处理逻辑仍然交由子类去实现 apply(...)

// 抽象基类:大多数节点都可以继承它,避免重复写“组装链”的样板代码
public abstract class AbstractLogicLink<T, D, R> implements ILogicLink<T, D, R> {

    // 指向“下一个处理者”的引用
    private ILogicLink<T, D, R> next;

    @Override
    public ILogicLink<T, D, R> next() {
        return next;
    }

    @Override
    public ILogicLink<T, D, R> appendNext(ILogicLink<T, D, R> next) {
        this.next = next;
        return next; // 返回 next 以便连续 append类似 builder
    }

    /**
     * 便捷方法:当前节点决定“交给下一个处理者”
     */
    protected R next(T requestParameter, D dynamicContext) throws Exception {
        // 直接把请求丢给下一个节点继续处理
        // 注意:这里假设 next 一定存在;实际项目里建议判空以免 NPE见下文改进建议
        return next.apply(requestParameter, dynamicContext);
    }
}

子类只需要继承 AbstractLogicLink 并实现 apply(...)

  • 能处理就处理(并可选择直接返回,终止链条)。
  • 不处理或处理后仍需后续动作,就 return next(requestParameter, dynamicContext) 继续传递。

4实现子类

@Component
public class AuthLink extends AbstractLogicLink<Request, Context, Response> {
    @Override
    public Response apply(Request req, Context ctx) throws Exception {
        if (!ctx.isAuthenticated()) {
            // 未认证:立刻终止;也可以在这里构造一个标准错误响应返回
            throw new UnauthorizedException();
        }
        // 认证通过,继续下一个环节
        return next(req, ctx);
    }
}
@Component
public class LoggingLink extends AbstractLogicLink<Request, Context, Response> {
    @Override
    public Response apply(Request req, Context ctx) throws Exception {
        System.out.println("Request received: " + req);
        return next(req, ctx);
    }
}
@Component
public class BusinessLogicLink extends AbstractLogicLink<Request, Context, Response> {
    @Override
    public Response apply(Request req, Context ctx) throws Exception {
        // 业务逻辑...
        return new Response(...);
    }
}

5组装链

@Configuration
@RequiredArgsConstructor
public class LogicChainFactory {

    private final AuthLink authLink;
    private final LoggingLink loggingLink;
    private final BusinessLogicLink businessLogicLink;

    @Bean
    public ILogicLink<Request, Context, Response> logicChain() {
        return authLink
                .appendNext(loggingLink)
                .appendNext(businessLogicLink);
    }
}

示例图:

AuthLink.apply
   └─▶ LoggingLink.apply
          └─▶ BusinessLogicLink.apply
                 └─▶ 返回 Response

这种模式链上的每个节点都手动 next()到下一节点。

多实例链1

以上是单例链即只能创建一条链比如A->B->C不能创建别的链因为节点Bean是单例的如果创别的链会导致指针引用错误

如果想变成多例链:

1节点由默认的单例模式改为原型模式

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class A extends AbstractLogicLink<Req, Ctx, Resp> { ... }

2组装链的时候注明不同链的bean名称

/** 全局唯一链A -> B -> C */
    @Bean("chainABC")
    public ILogicLink<Req, Ctx, Resp> chainABC() {
        A a = aProvider.getObject();
        B b = bProvider.getObject();
        C c = cProvider.getObject();
        return a.appendNext(b).appendNext(c);  // 返回链头 a
    }

    /** 全局唯一链A -> C */
    @Bean("chainAC")
    public ILogicLink<Req, Ctx, Resp> chainAC() {
        A a = aProvider.getObject();
        C c = cProvider.getObject();
        return a.appendNext(c);               // 返回链头 a另一套实例
    }

多实例链2

/**
 * 通用逻辑处理器接口 —— 责任链中的「节点」要实现的核心契约。
 */
public interface ILogicHandler<T, D, R> {

    /**
     * 默认的 next占位实现方便节点若不需要向后传递时直接返回 null。
     */
    default R next(T requestParameter, D dynamicContext) {
        return null;
    }

    /**
     * 节点的核心处理方法。
     */
    R apply(T requestParameter, D dynamicContext) throws Exception;

}

/**
 * 业务链路容器 —— 双向链表实现,同时实现 ILogicHandler从而可以被当作单个节点使用。
 */
public class BusinessLinkedList<T, D, R> extends LinkedList<ILogicHandler<T, D, R>> implements ILogicHandler<T, D, R>{

    public BusinessLinkedList(String name) {
        super(name);
    }

    /**
     * BusinessLinkedList是头节点它的apply方法就是循环调用后面的节点直至返回。
     * 遍历并执行链路。
     */
    @Override
    public R apply(T requestParameter, D dynamicContext) throws Exception {
        Node<ILogicHandler<T, D, R>> current = this.first;
        // 顺序执行,直到链尾或返回结果
        while (current != null) {
            ILogicHandler<T, D, R> handler = current.item;
            R result = handler.apply(requestParameter, dynamicContext);
            if (result != null) {
                // 节点命中,立即返回
                return result;
            }
            //result==null则交给那一节点继续处理
            current = current.next;
        }
        // 全链未命中
        return null;
    }
}
/**
 * 链路装配工厂 —— 负责把一组 ILogicHandler 顺序注册到 BusinessLinkedList 中。
 */
public class LinkArmory<T, D, R> {

    private final BusinessLinkedList<T, D, R> logicLink;

    /**
     * @param linkName      链路名称,便于日志排查
     * @param logicHandlers 节点列表,按传入顺序链接
     */
    @SafeVarargs
    public LinkArmory(String linkName, ILogicHandler<T, D, R>... logicHandlers) {
        logicLink = new BusinessLinkedList<>(linkName);
        for (ILogicHandler<T, D, R> logicHandler: logicHandlers){
            logicLink.add(logicHandler);
        }
    }

    /** 返回组装完成的链路 */
    public BusinessLinkedList<T, D, R> getLogicLink() {
        return logicLink;
    }

}

//工厂类可以定义多条责任链每条有自己的Bean名称区分。
@Bean("tradeRuleFilter")
public BusinessLinkedList<TradeRuleCommandEntity, DynamicContext, TradeRuleFilterBackEntity> tradeRuleFilter(ActivityUsabilityRuleFilter activityUsabilityRuleFilter, UserTakeLimitRuleFilter userTakeLimitRuleFilter) {
     // 1. 组装链
     LinkArmory<TradeRuleCommandEntity, DynamicContext, TradeRuleFilterBackEntity> linkArmory =
                new LinkArmory<>("交易规则过滤链", activityUsabilityRuleFilter, userTakeLimitRuleFilter);

     // 2. 返回链容器(即可作为责任链使用)
     return linkArmory.getLogicLink();
}

示例图:

BusinessLinkedList.apply  ←─ 只有这一层在栈里
while 循环:
   ├─▶ 调用 ActivityUsability.apply → 返回 null → 继续
   ├─▶ 调用 UserTakeLimit.apply   → 返回 null → 继续
   └─▶ 调用 ...                  → 返回 Result → break

链头拿着“游标”一个个跑,节点只告诉“命中 / 未命中”。

这里无需把节点改为原型模式,也可以实现多例链,因为由双向链表BusinessLinkedList 负责保存链路关系和推进执行,而ILogicHandler节点本身不再保存 next 指针,所以它们之间没有共享可变状态。

本项目中使用多实例链2有以下场景

一、拼团「锁单前」校验链

目标:在真正锁单前把“活动有效性 / 用户参与资格 / 可用库存”一口气校清楚,避免后续回滚。

1.活动有效性校验 ActivityUsability (当前时间是否早于活动截止时间)

2.用户可参与活动次数校验 UserTakeLimitRuleFilter(默认用户只可参与一次拼团)

3.剩余库存校验 TeamStockOccupyRuleFilter(可能同时有多人点击参与当前拼团,尝试抢占库存,仅部分人可通过校验。)

校验通过方可进行真正的锁单。

二、交易结算校验链

1.渠道黑名单校验 SCRuleFilter:某签约渠道下架/风控拦截,禁止结算。

2.外部单号校验 OutTradeNoRuleFilter:查营销订单;不存在或已退单(CLOSE)→ 不结算。

3.可结算时间校验 SettableRuleFilter:结算时间必须在拼团有效期内(outTradeTime < team.validEndTime),比如发起

拼团一个小时之内要结算完毕。

4.结束节点EndRuleFilter:整理上下文到返回对象,作为结算规则校验的产出。

检验通过方可进入真正的结算。

三、交易退单执行链

1.数据加载 DataNodeFilter:按 userId + outTradeNo 查询营销订单与拼团信息,写入上下文。

2.重复退单检查 UniqueRefundNodeFilter:订单已是 CLOSE → 视为幂等重复,直接返回。

3.退单策略执行 RefundOrderNodeFilter:依据“拼团态 + 订单态”选用具体退单策略 IRefundOrderStrategy,执行退款/解锁/改库并返回成功结果。

本身就是完整的退单流程。

规则树流程

image-20250725120957709

整体分层思路

分层 作用 关键对象
通用模板层 抽象出与具体业务无关的「规则树」骨架,解决 如何找到并执行策略 的共性问题 StrategyMapperStrategyHandlerAbstractStrategyRouter<T,D,R>
业务装配层 基于模板,自由拼装出 一棵 贴合业务流程的策略树 RootNode / SwitchNode / MarketNode / EndNode …
对外暴露层 通过 工厂 + 服务支持类 将整棵树封装成一个可直接调用的 StrategyHandler,并交给 Spring 整体托管 DefaultActivityStrategyFactoryAbstractGroupBuyMarketSupport

通用模板层:规则树的“骨架”

角色 职责 关系
StrategyMapper 映射器:依据 requestParameter + dynamicContext 选出 下一个 策略节点 AbstractStrategyRouter 调用
StrategyHandler 处理器:真正执行业务逻辑;apply 结束后可返回结果或继续路由 节点本身 / 路由器本身都是它的实现
AbstractStrategyRouter<T,D,R> 路由模板:① 调用 get(...) 找到合适的 StrategyHandler;② 调用该 handler 的 apply(...);③ 若未命中则走 defaultStrategyHandler 同时实现 StrategyMapperStrategyHandler,但自身保持 抽象,把细节延迟到子类

业务装配层:一棵可编排的策略树

RootNode  ->  SwitchNode  ->  MarketNode  ->  EndNode
                          ↘︎  OtherNode ...
  • 每个节点

    继承 AbstractGroupBuyMarketSupport(业务基类)

    • 实现 get(...):决定当前节点的下一跳是哪一个节点
    • 实现 apply(...):实现节点自身应做的业务动作(或继续下钻)
  • 组合方式

    • 路由是“数据驱动”的:并非工厂把链写死,而是节点在运行期根据 request + context 决定下一跳可能是ERROR_NODE或END_NODE灵活插拔。

对外暴露层:工厂 + 服务支持类

组件 主要职责
DefaultActivityStrategyFactory (@Service) 仅负责把 RootNode 暴露为 StrategyHandler 入口(交由 Spring 管理,方便注入)。
AbstractGroupBuyMarketSupport 业务服务基类:封装拼团场景下共用的查询、工具方法;供每个节点继承使用

本项目执行总览:

调用入口factory.strategyHandler() → 返回 RootNode(实现了 StrategyHandler)。

执行流程

apply(...):模板入口,multiThread(...) 预取/并发任务,doApply(...)

doApply(...):每个节点自己的业务;通常在末尾调用 router(...) 继续下一个节点(你现在就是这样写的:return router(request, ctx);)。也可以在某些节点“短路返回”,不再路由。

router(...):内部调用当前节点的 get(...)挑选下一节点next,若存在就调用 next.apply(...) 递归推进;若不存在(或是到达 EndNode),则收束返回

RootNode

  • 校验必填:userId/goodsId/source/channel
  • 合法则路由到 SwitchNode;非法直接抛 ILLEGAL_PARAMETER

SwitchNode总开关、不区分活动做总体的降级限流

  • 调用 repository.downgradeSwitch() 判断是否降级;是则抛 E0003
  • 调用 repository.cutRange(userId) 做切量;不在范围抛 E0004
  • 通过后路由到 MarketNode

MarketNode

  • multiThread(...) 中并发拉取:
    • 拼团活动配置 GroupBuyActivityDiscountVO
    • 商品信息 SkuVO
    • 写入 DynamicContext
  • doApply(...)
    • 读取配置 + SKUmarketPlanIDiscountCalculateService,计算 payPrice / deductionPrice 并写回上下文。
  • 路由判定:
    • 若配置/商品/deductionPrice 有缺失 → ErrorNode
    • 否则 → TagNode

TagNode业务相关部分人不在本次活动范围内

  • 若活动没配置 tagId → 视为不限定人群:visible=trueenable=true
  • 否则通过 repository.isTagCrowdRange(tagId, userId) 判断是否在人群内,并据此更新 visible/enable
  • 路由到 EndNode

EndNode

  • DynamicContext 读取:skuVO / payPrice / deductionPrice / groupBuyActivityDiscountVO / visible / enable
  • 构建并返回最终的 TrialBalanceEntity,链路终止。

ErrorNode

  • 统一异常出口;若无配置/无商品,抛 E0002;否则可返回空结果作为兜底;
  • 返回后走 defaultStrategyHandler(结束)。