Knowledge note
group-buy-market-types 模块学习文档
group-buy-market-types 模块学习文档
模块定位:全局通用基础类型层
包路径:cn.bugstack.types
依赖关系:不依赖任何项目内其他模块,被所有模块依赖
源码文件:7 个 Java 类 / 枚举
#目录
- 模块概述
- 模块结构图
- Constants — 通用常量
- ActivityStatusEnumVO — 活动状态枚举
- GroupBuyOrderEnumVO — 拼团订单状态枚举
- NotifyTaskHTTPEnumVO — 回调任务状态枚举
- ResponseCode — 统一响应码
- AppException — 业务异常
- BaseEvent — 事件基类
- 综合示例
- 学习要点总结
#1. 模块概述
group-buy-market-types 是 DDD 六层架构中的最底层公共模块。它定义了整个项目共用的常量、枚举、异常和事件基类,确保所有模块在数据类型定义上保持一致。
设计原则:
- 零外部依赖:不依赖任何其他项目模块(
group-buy-market-*),只依赖 JDK + Lombok - 纯值承载:所有类都不包含业务逻辑,只承载值定义
- 被所有模块反向依赖:api、domain、infrastructure、trigger、app 都可以引用 types
模块职责边界:
┌────────────────────────────────────────────────┐
│ types 模块 │
│ │
│ common/ ─── Constants (字符串常量) │
│ enums/ ─── ResponseCode (统一错误码) │
│ ── ActivityStatusEnumVO │
│ ── GroupBuyOrderEnumVO │
│ ── NotifyTaskHTTPEnumVO │
│ event/ ─── BaseEvent (MQ 事件基类) │
│ exception/ ─ AppException (业务异常) │
└────────────────────────────────────────────────┘
↑ ↑ ↑
api 模块 domain 模块 infrastructure
trigger 模块
app 模块
#2. 模块结构图
group-buy-market-types/
└── src/main/java/cn/bugstack/types/
├── common/
│ └── Constants.java # 通用字符串常量
├── enums/
│ ├── ActivityStatusEnumVO.java # 拼团活动状态
│ ├── GroupBuyOrderEnumVO.java # 拼团队伍状态
│ ├── NotifyTaskHTTPEnumVO.java # 回调任务执行状态
│ └── ResponseCode.java # 统一响应码(全系统错误码中心)
├── event/
│ └── BaseEvent.java # MQ 事件消息抽象基类
└── exception/
└── AppException.java # 业务异常(带错误码的 RuntimeException)
文件职责速查:
| 文件 | 职责 | 使用者 |
|---|---|---|
Constants.java | 保存字符串字面量,如分隔符 , 和下划线 _ | infrastructure, domain |
ActivityStatusEnumVO.java | 活动生命周期 创建 → 生效 → 过期 → 废弃 | domain (ActivityUsabilityRuleFilter)、infrastructure (TradeRepository) |
GroupBuyOrderEnumVO.java | 拼团队伍状态 拼单中/完成/失败/完成含退单 | domain (退单策略、结算过滤器)、infrastructure (TradeRepository) |
NotifyTaskHTTPEnumVO.java | HTTP 回调执行结果 成功/失败/空执行 | domain (TradeTaskService)、infrastructure (TradePort) |
ResponseCode.java | 系统全局错误码,区分通用码和业务码 | 所有模块 |
BaseEvent.java | 所有 MQ 事件的抽象父类 | infrastructure (EventPublisher) |
AppException.java | 携带错误码的 RuntimeException | 所有模块的 domain 层规则过滤 |
#3. Constants — 通用常量
源码:cn.bugstack.types.common.Constants
package cn.bugstack.types.common;
public class Constants {
public final static String SPLIT = ",";
public final static String UNDERLINE = "_";
}
文件分析:
这是一个极简的常量类,只定义了两个分隔符常量。在项目中作为避免字符串硬编码的工具使用。
为什么要单独定义一个类?
如果不抽取常量,代码中会出现大量魔法字符串:
// 差:硬编码分隔符,散布各处
String[] parts = tagScope.split(","); // 位置 A
String[] parts = marketExpr.split(","); // 位置 B
String bizId = activityId + "_" + userId; // 位置 C
String bizId = teamId + "_" + category; // 位置 D
// 好:统一常量,修改一处即可
String[] parts = tagScope.split(Constants.SPLIT);
String bizId = activityId + Constants.UNDERLINE + userId;
项目中的使用位置:
| 文件 | 使用方式 | 用途 |
|---|---|---|
DCCService.java | scBlacklist.split(Constants.SPLIT) | 解析 DCC 配置的黑名单列表 |
MJCalculateService.java | marketExpr.split(Constants.SPLIT) | 解析满减表达式 "100,20" |
GroupBuyActivityDiscountVO.java | tagScope.split(Constants.SPLIT) | 解析人群标签范围 "1,2" |
TradeRepository.java | a + Constants.UNDERLINE + b | 构建 bizId / uuid / lockKey 等唯一键 |
设计模式笔记:
- 这种写法属于
Constant Class Pattern(常量类模式),适合少量、无分类的全局常量 - 如果常量变多(几十个),应拆分为更细粒度的常量类(如
RedisKeyConstants、BizRuleConstants)
#4. ActivityStatusEnumVO — 活动状态枚举
源码:cn.bugstack.types.enums.ActivityStatusEnumVO
@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum ActivityStatusEnumVO {
CREATE(0, "创建"),
EFFECTIVE(1, "生效"),
OVERDUE(2, "过期"),
ABANDONED(3, "废弃"),
;
private Integer code;
private String info;
public static ActivityStatusEnumVO valueOf(Integer code) {
switch (code) {
case 0: return CREATE;
case 1: return EFFECTIVE;
case 2: return OVERDUE;
case 3: return ABANDONED;
}
throw new RuntimeException("err code not exist!");
}
}
状态流转图:
┌─────────────┐
│ CREATE │ ← 活动刚创建,尚未对外
│ (0, 创建) │
└──────┬──────┘
│ 运营配置完成,发布
▼
┌─────────────┐
┌────────────→│ EFFECTIVE │ ← 活动在线,用户可参与
│ 重新生效 │ (1, 生效) │
│ └──────┬──────┘
│ │ 活动时间到期
│ ▼
│ ┌─────────────┐
│ │ OVERDUE │ ← 自然到期
│ │ (2, 过期) │
│ └──────────────┘
│
│ 活动开始前主动下线
│ │
│ ▼
│ ┌─────────────┐
└──────────── │ ABANDONED │ ← 人工废弃
│ (3, 废弃) │
└──────────────┘
为什么命名以 EnumVO 结尾?
这里的 "VO" 代表值对象(Value Object)。在 DDD 中,枚举本身就是一种值对象——它们没有独立身份,通过值来比较相等性。作者用 EnumVO 后缀区分于普通枚举,强调它们是领域模型中的值对象角色。
代码中使用位置:
| 文件 | 使用方式 |
|---|---|
ActivityUsabilityRuleFilter.java | 校验活动状态是否为 EFFECTIVE,不是则抛 AppException(ResponseCode.E0101) |
TradeRepository.java | 查询活动时映射 DB 的 status 字段为枚举,确保代码不直接依赖整数 |
GroupBuyActivityEntity.java | 实体内部持有此枚举类型 |
设计要点:
@NoArgsConstructor是给 MyBatis 反序列化用的,框架需要无参构造- 自定义
valueOf(Integer)是因为数字枚举无法直接通过字段反查 — 这是 JDK enum 的限制 - 抛
RuntimeException而非受检异常,因为错误码不存在意味着代码 bug,无法运行时恢复
#5. GroupBuyOrderEnumVO — 拼团订单状态枚举
源码:cn.bugstack.types.enums.GroupBuyOrderEnumVO
@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum GroupBuyOrderEnumVO {
PROGRESS(0, "拼单中"),
COMPLETE(1, "完成"),
FAIL(2, "失败"),
COMPLETE_FAIL(3, "完成-含退单"),
;
private Integer code;
private String info;
public static GroupBuyOrderEnumVO valueOf(Integer code) {
// ... switch-case 反查
}
}
状态说明与业务含义:
| 状态 | 语义 | 触发条件 |
|---|---|---|
PROGRESS (0) | 拼单中 | 队伍创建后默认状态,至少 1 人锁单但未达目标 |
COMPLETE (1) | 完成 | complete_count >= target_count,拼团成功 |
FAIL (2) | 失败 | 有效期到期后 complete_count < target_count,拼团失败 |
COMPLETE_FAIL (3) | 完成-含退单 | 已完成的团,但有人退单(退款了一部分) |
状态流转:
首次锁单
│
▼
┌──────────┐ 全部人支付完成
│ PROGRESS │ ──────────────────────→ ┌──────────┐
│ (拼单中) │ │ COMPLETE │
└────┬─────┘ │ (完成) │
│ └────┬─────┘
│ │ 有人退单
│ 超时未成团 ▼
▼ ┌──────────────┐
┌──────────┐ │ COMPLETE_FAIL│
│ FAIL │ │ (完成-含退单) │
│ (失败) │ └──────────────┘
└──────────┘
在项目中的使用:
TradeRepository.java:更新队伍状态时使用此枚举做类型安全检查GroupBuyTeamEntity.java:实体持有枚举而非原始的 intTradeSettlementRuleFilterBackEntity.java:过滤结果中携带队伍状态- 退单策略中:根据订单状态决定走哪一种退单策略
- PROGRESS 且 order outTradeTime 为空 →
Unpaid2RefundStrategy(未支付退单) - 已支付但队伍未完成 →
Paid2RefundStrategy(已支付个人退单) - 已完成后退单 →
PaidTeam2RefundStrategy(拼团完成后有人退单)
- PROGRESS 且 order outTradeTime 为空 →
#6. NotifyTaskHTTPEnumVO — 回调任务状态枚举
源码:cn.bugstack.types.enums.NotifyTaskHTTPEnumVO
@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum NotifyTaskHTTPEnumVO {
SUCCESS("success", "成功"),
ERROR("error", "失败"),
NULL(null, "空执行"),
;
private String code;
private String info;
}
特别之处:
注意 code 类型为 String 而非 Integer,且 NULL 的 code 值为 null。这是三个枚举中唯一使用字符串 code 的枚举。
为什么 code 用 String?
因为外部 HTTP 调用的响应通常返回字符串状态(如 "success" / "error"),使用 String 避免了多余的类型转换。
NULL 状态的用途:
NULL 表示"不需要执行回调"。在结算时,可能当前业务不需要做任何通知,这时用 NULL 状态标记,避免定时任务误扫这类任务。
项目中使用:
| 文件 | 使用方式 |
|---|---|
TradeTaskService.java | 执行回调后根据 HTTP 响应返回 SUCCESS 或 ERROR |
TradePort.java | 调用外部 HTTP 接口后,将 response 映射为此枚举 |
TradeSettlementOrderService.java | 创建回调任务时的状态判断 |
#7. ResponseCode — 统一响应码
源码:cn.bugstack.types.enums.ResponseCode
@AllArgsConstructor
@NoArgsConstructor
@Getter
public enum ResponseCode {
// ========== 通用系统码 ==========
SUCCESS("0000", "成功"),
UN_ERROR("0001", "未知失败"),
ILLEGAL_PARAMETER("0002", "非法参数"),
INDEX_EXCEPTION("0003", "唯一索引冲突"),
UPDATE_ZERO("0004", "更新记录为0"),
HTTP_EXCEPTION("0005", "HTTP接口调用异常"),
RATE_LIMITER("0006", "接口限流"),
// ========== 业务异常码 E000x ==========
E0001("E0001", "不存在对应的折扣计算服务"),
E0002("E0002", "无拼团营销配置"),
E0003("E0003", "拼团活动降级拦截"),
E0004("E0004", "拼团活动切量拦截"),
E0005("E0005", "拼团组队失败,记录更新为0"),
E0006("E0006", "拼团组队完结,锁单量已达成"),
E0007("E0007", "拼团人群限定,不可参与"),
E0008("E0008", "拼团组队失败,缓存库存不足"),
// ========== 业务异常码 E010x ==========
E0101("E0101", "拼团活动未生效"),
E0102("E0102", "不在拼团活动有效时间内"),
E0103("E0103", "当前用户参与此拼团次数已达上限"),
E0104("E0104", "不存在的外部交易单号或用户已退单"),
E0105("E0105", "SC渠道黑名单拦截"),
E0106("E0106", "订单交易时间不在拼团有效时间范围内"),
;
private String code;
private String info;
}
错误码编码规范:
0000 ~ 0006 通用系统级别错误
E0001 ~ E0008 业务异常 - 活动/试算/锁单/库存
E0101 ~ E0106 业务异常 - 规则过滤链(活动可用性、用户限制、渠道、时间)
错误码在锁单全链路的抛出示意:
RootNode ────────→ 参数为空 → ILLEGAL_PARAMETER (0002)
│
▼
SwitchNode ──────→ 无营销配置 → E0002
│ → 折扣服务不存在 → E0001
▼
TagNode ─────────→ 人群不匹配 → E0007
│
▼
MarketNode ──────→ 无对应折扣类型 → E0001
│
▼
ActivityUsabilityRuleFilter → 未生效 → E0101
→ 不在时间范围 → E0102
│
▼
UserTakeLimitRuleFilter → 超限 → E0103
│
▼
TeamStockOccupyRuleFilter → 库存不足 → E0008
在 Controller 层的消费方式:
// MarketTradeController.java
try {
// ... 业务逻辑
} catch (AppException e) {
return Response.<XXX>builder()
.code(e.getCode()) // ← 直接从异常中取 code
.info(e.getInfo())
.build();
} catch (Exception e) {
return Response.<XXX>builder()
.code(ResponseCode.UN_ERROR.getCode()) // ← 未知异常用通用码
.info(ResponseCode.UN_ERROR.getInfo())
.build();
}
#8. AppException — 业务异常
源码:cn.bugstack.types.exception.AppException
@EqualsAndHashCode(callSuper = true)
@Data
public class AppException extends RuntimeException {
private static final long serialVersionUID = 5317680961212299217L;
private String code;
private String info;
// 1) 仅错误码
public AppException(String code) { this.code = code; }
// 2) 通过 ResponseCode 枚举构造(最常用)
public AppException(ResponseCode responseCode) {
this.code = responseCode.getCode();
this.info = responseCode.getInfo();
}
// 3) 错误码 + 原始异常
public AppException(String code, Throwable cause) {
this.code = code;
super.initCause(cause);
}
// 4) 错误码 + 自定义消息
public AppException(String code, String message) {
this.code = code;
this.info = message;
}
// 5) 完整:错误码 + 自定义消息 + 原始异常
public AppException(String code, String message, Throwable cause) {
this.code = code;
this.info = message;
super.initCause(cause);
}
@Override
public String toString() {
return "cn.bugstack.types.exception.AppException{" +
"code='" + code + '\'' + ", info='" + info + '\'' + '}';
}
}
设计分析:
为什么继承 RuntimeException 而非 Exception?
DDD 的业务异常通常不做强制 try-catch。业务规则违反(如"活动未生效"、"库存不足")是无法通过代码正常恢复的,只能在最外层(Controller)统一捕获并返回提示。如果继承 Exception,整个调用链上所有的 filter 方法都要声明 throws,造成噪音。
五种构造函数的适用场景:
| 构造方式 | 场景 | 代码示例 |
|---|---|---|
new AppException(ResponseCode.E0101) | 最常见的规则过滤抛异常 | ActivityUsabilityRuleFilter |
new AppException(code, message) | 需要附加动态信息 | throw new AppException("E0103", "用户 xfg01 已达上限" + limit) |
new AppException(code, cause) | 包装底层异常 | 数据库异常向上转换 |
new AppException(code, message, cause) | 完整上下文 | 需要同时传递自定义消息和原始异常 |
在责任链 Filter 中的典型用法:
// ActivityUsabilityRuleFilter.java - 典型的规则过滤模式
public TradeLockRuleFilterBackEntity apply(...) throws Exception {
GroupBuyActivityEntity groupBuyActivity = repository.queryGroupBuyActivityEntityByActivityId(...);
// 业务规则不满足 → 直接抛 AppException
if (!ActivityStatusEnumVO.EFFECTIVE.equals(groupBuyActivity.getStatus())) {
throw new AppException(ResponseCode.E0101); // "拼团活动未生效"
}
// ... 继续下一个 filter
return next(requestParameter, dynamicContext);
}
异常传播路径:
Filter / Node / Service (domain 层)
│ throw new AppException(ResponseCode.E0101)
▼
Controller (trigger 层)
│ catch (AppException e)
│ return Response(code=e.getCode(), info=e.getInfo())
▼
HTTP Response → 前端展示 "拼团活动未生效"
#9. BaseEvent — 事件基类
源码:cn.bugstack.types.event.BaseEvent
@Data
public abstract class BaseEvent<T> {
public abstract EventMessage<T> buildEventMessage(T data);
public abstract String topic();
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class EventMessage<T> {
private String id; // 消息唯一 ID
private Date timestamp; // 消息时间戳
private T data; // 消息体(泛型)
}
}
设计分析:
这是一个模板方法模式,两个抽象方法要求子类必须提供:
topic()— 定义此事件投递到哪个 RabbitMQ 路由buildEventMessage(T data)— 定义如何将业务数据包装为消息
与 EventPublisher 的协作:
// infrastructure/event/EventPublisher.java(实际发布者)
@Component
public class EventPublisher {
@Autowired
private RabbitTemplate rabbitTemplate;
@Value("${spring.rabbitmq.config.producer.exchange}")
private String exchangeName;
public void publish(String routingKey, String message) {
rabbitTemplate.convertAndSend(exchangeName, routingKey, message, m -> {
m.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return m;
});
}
}
当前版本的状态:
BaseEvent 的 topic() 和 buildEventMessage() 目前并未被直接调用。实际的 MQ 消息发送走的是硬编码路径(tradeTaskService 和 TradePort 内部调用 EventPublisher.publish(routingKey, jsonString))。
BaseEvent 的意图是为未来扩展预留:
- 当消息种类增多时,每种子类实现自己的
topic()和buildEventMessage() - 发布者可以统一写成
eventPublisher.publish(event.buildEventMessage(data)) - 这样的设计避免了每个服务各自拼接消息格式和路由 key
典型子类扩展方向:
// 示例:拼团成功事件(当前项目尚未有此实现)
public class TeamSuccessEvent extends BaseEvent<TeamSuccessData> {
@Override
public String topic() {
return "topic.team_success";
}
@Override
public EventMessage<TeamSuccessData> buildEventMessage(TeamSuccessData data) {
return EventMessage.<TeamSuccessData>builder()
.id(UUID.randomUUID().toString())
.timestamp(new Date())
.data(data)
.build();
}
}
#10. 综合示例
下面以"用户锁单时活动不可用"的完整调用链,展示 types 各组件如何协同工作:
【1】用户 POST /api/v1/gbm/trade/lock_market_pay_order
↓
【2】MarketTradeController.lockMarketPayOrder()
↓
【3】TradeLockOrderService.lockMarketPayOrder()
↓
【4】→ tradeRuleFilter.apply(...) → 责任链依次调用
↓
【5】ActivityUsabilityRuleFilter.apply()
↓
GroupBuyActivityEntity groupBuyActivity = repository.queryGroupBuyActivityEntityByActivityId(100123);
// DB 返回 status = 0
if (!ActivityStatusEnumVO.EFFECTIVE.equals(groupBuyActivity.getStatus())) {
// ActivityStatusEnumVO.EFFECTIVE 值为 1(生效)
// groupBuyActivity.getStatus() 值为 0(CREATE)
// 不相等 → 进入 if 分支
throw new AppException(ResponseCode.E0101);
// .getCode() → "E0101"
// .getInfo() → "拼团活动未生效"
}
↓
【6】异常向上传播至 MarketTradeController
↓
【7】Controller 的 catch (AppException e) 分支
↓
构建统一响应:
Response<LockMarketPayOrderResponseDTO>.builder()
.code("E0101")
.info("拼团活动未生效")
.build();
↓
【8】HTTP 200 返回 JSON:
{
"code": "E0101",
"info": "拼团活动未生效",
"data": null
}
涉及的 types 组件:
| 步骤 | 组件 | 角色 |
|---|---|---|
| 5 | ActivityStatusEnumVO.EFFECTIVE | 领域枚举值,表达"生效"这一业务概念 |
| 5 | ResponseCode.E0101 | 统一错误码,用于构造异常 |
| 5 | AppException(ResponseCode) | 携带错误码的异常,中断责任链 |
| 7 | ResponseCode.UN_ERROR | 未知异常时的兜底错误码 |
#11. 学习要点总结
#11.1 设计模式
| 模式 | 体现位置 | 说明 |
|---|---|---|
| 值对象 (Value Object) | 四个枚举类 | 枚举是不可变值对象,通过值比较相等性,无独立标识 |
| 模板方法 | BaseEvent | 定义 topic() 和 buildEventMessage() 抽象方法,子类填充 |
| 常量类 | Constants | 集中管理全局字符串字面量,消除魔法值 |
#11.2 关键设计决策
- 枚举后缀
EnumVO:显式声明枚举属于领域值对象,与普通的 Java enum 区分语义 AppException继承RuntimeException:避免污染整个调用链的throws声明ResponseCode独立为枚举而非类:确保编译时检查错误码引用,IDE 自动提示Constants只存两个值:不贪多,只存真正在各处重复使用的常量,防止常量类膨胀BaseEvent预留但未用:架构上的前瞻设计,为 MQ 消息标准化预留位置
#11.3 对初学者的启示
- 看源码先看 types:理解全局枚举和错误码,后面看业务流程时能迅速理解每种异常的触发条件
- 错误码编号有规律:E000x 是活动/锁单,E010x 是规则过滤,按模块分段维护
- 枚举命名模式:
valueOf(Integer code)是手动写的反查方法,因为 Java enum 不支持自动从字段反推实例 - 最小依赖原则:types 不依赖任何项目内部模块,只靠 JDK+Lombok,保证在所有环境都可编译