Knowledge note
正在加载知识笔记
正在加载知识笔记
Knowledge note
模块定位:API 契约层 / 分布式调用接口定义
包路径:cn.bugstack.api
依赖关系:仅依赖 JDK + Lombok,被 app/trigger 层实现,被外部调用方依赖
源码文件:11 个 Java 类/接口
group-buy-market-api 在 DDD 六层架构中扮演的是接口契约角色。它不包含任何实现代码,只定义:
Response<T> 泛型包装类 ┌──────────────────────────────┐
│ group-buy-market-api │
│ (纯接口 + DTO,无实现) │
└──────────┬───────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ 调用方 A │ │ 调用方 B │ │ 本项目 app │
│(其他服务) │ │(其他服务) │ │ (实现接口) │
└──────────┘ └──────────┘ └──────────────┘
好处:
trigger / app ← 实现 api 中定义的接口
│
│ implements
▼
api ← 你当前的位置 (契约定义层)
│
│ uses types
▼
domain ← 业务逻辑
依赖规则:
String、Long、BigDecimal 等,不能使用 domain 的 TrialBalanceEntity 等实体group-buy-market-api/
└── src/main/java/cn/bugstack/api/
├── IDCCService.java # 动态配置中心接口
├── IMarketIndexService.java # 首页营销服务接口
├── IMarketTradeService.java # 交易服务接口
├── response/
│ └── Response.java # 统一响应包装(泛型)
└── dto/
├── GoodsMarketRequestDTO.java # 商品营销查询请求
├── GoodsMarketResponseDTO.java # 商品营销查询响应(含内嵌类)
├── LockMarketPayOrderRequestDTO.java # 锁单请求(含内嵌 NotifyConfigVO)
├── LockMarketPayOrderResponseDTO.java # 锁单响应
├── SettlementMarketPayOrderRequestDTO.java # 结算请求
├── SettlementMarketPayOrderResponseDTO.java # 结算响应
├── RefundMarketPayOrderRequestDTO.java # 退单请求
├── RefundMarketPayOrderResponseDTO.java # 退单响应
└── NotifyRequestDTO.java # 回调通知请求
文件职责速查:
| 文件 | 职责 | 实现者 |
|---|---|---|
IDCCService.java | 动态配置中心开关控制接口 | app 模块 |
IMarketIndexService.java | 首页商品详情页的拼团价展示 | app 模块 |
IMarketTradeService.java | 锁单、结算、退单三个交易操作 | app 模块 |
Response.java | 统一响应格式 {code, info, data} | 所有接口的返回包装 |
GoodsMarketRequestDTO.java | 商品查询入参(userId, goodsId, source, channel) | — |
GoodsMarketResponseDTO.java | 商品查询出参(商品 + 队伍列表 + 统计) | — |
LockMarketPayOrderRequestDTO.java | 锁单入参(含回调配置) | — |
LockMarketPayOrderResponseDTO.java | 锁单出参(orderId, 价格, teamId) | — |
SettlementMarketPayOrderRequestDTO.java | 结算入参 | — |
SettlementMarketPayOrderResponseDTO.java | 结算出参 | — |
RefundMarketPayOrderRequestDTO.java | 退单入参 | — |
RefundMarketPayOrderResponseDTO.java | 退单出参(含行为状态码) | — |
NotifyRequestDTO.java | 外部回调请求(teamId + outTradeNoList) | — |
源码:cn.bugstack.api.response.Response
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Response<T> implements Serializable {
private static final long serialVersionUID = 7000723935764546321L;
private String code; // 响应码("0000"=成功)
private String info; // 响应消息
private T data; // 泛型数据体
}
Response<T> ResponseCode (types)
┌─────────────────────┐ ┌──────────────────────┐
│ code: "0000" │ ←── 对应 ──│ SUCCESS("0000", "成功") │
│ info: "成功" │ │ UN_ERROR("0001", ...) │
│ data: { ... } │ │ E0001("E0001", ...) │
└─────────────────────┘ └──────────────────────┘
Response.code 的值来自 ResponseCode 枚举String code 而非 ResponseCode 枚举类型ResponseCode 转换为 String 填入 ResponseResponse<T>?// 首页营销 - data 是 GoodsMarketResponseDTO
Response<GoodsMarketResponseDTO>
// 锁单 - data 是 LockMarketPayOrderResponseDTO
Response<LockMarketPayOrderResponseDTO>
// 退单 - data 是 RefundMarketPayOrderResponseDTO
Response<RefundMarketPayOrderResponseDTO>
一个包装类适配所有接口的返回,避免为每个接口写一个 Response 类。
public interface IMarketIndexService {
Response<GoodsMarketResponseDTO> queryGroupBuyMarketConfig(
GoodsMarketRequestDTO goodsMarketRequestDTO);
}
业务场景:用户打开商品详情页时,前端调用此接口获取该商品的拼团信息。
映射关系:
| 接口 | → domain 入口 | → 返回值 |
|---|---|---|
IMarketIndexService | IIndexGroupBuyMarketService | TrialBalanceEntity |
app 层实现此接口时,会调用 domain 的 IIndexGroupBuyMarketService.indexMarketTrial() + queryInProgressUserGroupBuyOrderDetailList() + queryTeamStatisticByActivityId(),把三个 domain 结果组合成一个 GoodsMarketResponseDTO。
@Data
public class GoodsMarketRequestDTO {
private String userId; // 用户ID
private String source; // 来源
private String channel; // 渠道
private String goodsId; // 商品ID
}
这四个字段完整映射到 domain 层的 MarketProductEntity。
结构比较复杂,包含三个内嵌类,对应页面需要的三块数据。详见第 8 节。
public interface IMarketTradeService {
// 1. 锁单(用户点击"参与拼团"时)
Response<LockMarketPayOrderResponseDTO> lockMarketPayOrder(
LockMarketPayOrderRequestDTO requestDTO);
// 2. 结算(用户支付完成时)
Response<SettlementMarketPayOrderResponseDTO> settlementMarketPayOrder(
SettlementMarketPayOrderRequestDTO requestDTO);
// 3. 退单(用户取消订单/退款时)
Response<RefundMarketPayOrderResponseDTO> refundMarketPayOrder(
RefundMarketPayOrderRequestDTO requestDTO);
}
三个方法对应交易生命周期的三个阶段:
lockMarketPayOrder → 下单锁单(预占优惠)
settlementMarketPayOrder → 支付结算(确认成团)
refundMarketPayOrder → 退单(逆向流程)
| 接口方法 | domain 入口 | domain 接口 |
|---|---|---|
lockMarketPayOrder | TradeLockOrderService | ITradeLockOrderService.lockMarketPayOrder() |
settlementMarketPayOrder | TradeSettlementOrderService | ITradeSettlementOrderService.settlementMarketPayOrder() |
refundMarketPayOrder | TradeRefundOrderService | ITradeRefundOrderService.refundOrder() |
public interface IDCCService {
Response<Boolean> updateConfig(String key, String value);
}
业务场景:运营人员通过后台动态调整配置(如降级开关、切量比例),无需重启服务。
这是一种运维管控接口,与拼团业务直接无关,但决定了拼团功能是否对用户可见(SwitchNode 的降级/切量判断依赖这些配置)。
| DTO | 字段 | 对应操作 |
|---|---|---|
GoodsMarketRequestDTO | userId, source, channel, goodsId | 首页查询 |
LockMarketPayOrderRequestDTO | userId, teamId, activityId, goodsId, source, channel, outTradeNo, notifyConfigVO | 锁单 |
SettlementMarketPayOrderRequestDTO | source, channel, userId, outTradeNo, outTradeTime | 结算 |
RefundMarketPayOrderRequestDTO | userId, outTradeNo, source, channel | 退单 |
NotifyRequestDTO | teamId, outTradeNoList | 外部回调 |
| DTO | 核心字段 | 来源 |
|---|---|---|
GoodsMarketResponseDTO | goods, teamList[], teamStatistic | 3 个 domain 调用合并 |
LockMarketPayOrderResponseDTO | orderId, originalPrice, deductionPrice, payPrice, tradeOrderStatus, teamId | domain MarketPayOrderEntity |
SettlementMarketPayOrderResponseDTO | userId, teamId, activityId, outTradeNo | domain TradePaySettlementEntity |
RefundMarketPayOrderResponseDTO | userId, orderId, teamId, code, info | domain TradeRefundBehaviorEntity |
以锁单为例:
LockMarketPayOrderRequestDTO PayActivityEntity (domain)
┌──────────────────────┐ ┌───────────────────────┐
│ userId: String │ ──→ │ (userId 在 UserEntity) │
│ teamId: String │ ──→ │ teamId: String │
│ activityId: Long │ ──→ │ activityId: Long │
│ goodsId: String │ ──→ │ (goodsId在PayDiscount) │
│ source: String │ ──→ │ source: String │
│ channel: String │ ──→ │ channel: String │
│ outTradeNo: String │ ──→ │ (透传,不做转换) │
│ notifyConfigVO │ ──→ │ (写入 team 配置) │
└──────────────────────┘ └───────────────────────┘
LockMarketPayOrderResponseDTO MarketPayOrderEntity (domain)
┌──────────────────────┐ ┌───────────────────────────┐
│ orderId: String │ ←── │ orderId: String │
│ originalPrice: BigDecimal ←── │ originalPrice: BigDecimal │
│ deductionPrice: BigDecimal ←── │ deductionPrice: BigDecimal│
│ payPrice: BigDecimal │ ←── │ payPrice: BigDecimal │
│ tradeOrderStatus: Int │ ←── │ tradeOrderStatusEnumVO │
│ teamId: String │ ←── │ teamId: String │
└──────────────────────┘ └───────────────────────────┘
关键转换:tradeOrderStatusEnumVO 是 domain 的枚举类型,但 DTO 只能用 Integer,所以 app 层需要做 TradeOrderStatusEnumVO.getCode() 转换。
这是 api 模块最复杂的 DTO,包含三级嵌套结构。
@Data @Builder
public class GoodsMarketResponseDTO {
private Long activityId; // 活动ID
private Goods goods; // 商品信息(含价格)
private List<Team> teamList; // 组队列表
private TeamStatistic teamStatistic; // 组队统计
// 内嵌类1:商品信息
public static class Goods {
private String goodsId;
private BigDecimal originalPrice; // 原价
private BigDecimal deductionPrice; // 折扣金额
private BigDecimal payPrice; // 实付
}
// 内嵌类2:组队信息(含倒计时计算)
public static class Team {
private String userId;
private String teamId;
private Long activityId;
private Integer targetCount; // 目标人数
private Integer completeCount; // 已完成人数
private Integer lockCount; // 已锁单人数
private Date validStartTime;
private Date validEndTime;
private String validTimeCountdown; // 倒计时字符串 "02:30:00"
private String outTradeNo;
// 工具方法:时间差转倒计时字符串
public static String differenceDateTime2Str(Date start, Date end) {
// diffInMilliseconds → "HH:mm:ss" 格式
}
}
// 内嵌类3:组队统计
public static class TeamStatistic {
private Integer allTeamCount; // 开团队伍总数
private Integer allTeamCompleteCount; // 成团队伍数
private Integer allTeamUserCount; // 总参团人数
}
}
GoodsMarketResponseDTO
│
├── activityId ← TrialBalanceEntity.groupBuyActivityDiscountVO.activityId
│
├── goods ← TrialBalanceEntity (goodsId, originalPrice, deductionPrice, payPrice)
│
├── teamList[] ← IIndexGroupBuyMarketService.queryInProgressUserGroupBuyOrderDetailList()
│ (包含用户自己的 + 随机他人的进行中拼团)
│
└── teamStatistic ← IIndexGroupBuyMarketService.queryTeamStatisticByActivityId()
differenceDateTime2Str 工具方法public static String differenceDateTime2Str(Date validStartTime, Date validEndTime) {
if (validStartTime == null || validEndTime == null) return "无效的时间";
long diffInMilliseconds = validEndTime.getTime() - validStartTime.getTime();
if (diffInMilliseconds < 0) return "已结束";
long hours = TimeUnit.MILLISECONDS.toHours(diffInMilliseconds) % 24;
long minutes = TimeUnit.MILLISECONDS.toMinutes(diffInMilliseconds) % 60;
long seconds = TimeUnit.MILLISECONDS.toSeconds(diffInMilliseconds) % 60;
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
}
这个方法放在 DTO 的内嵌类里,因为它是纯展示逻辑,与业务无关 —— 前端需要一个倒计时字符串,DTO 直接提供。
@Data
public class LockMarketPayOrderRequestDTO {
private String userId;
private String teamId; // 可为空(null = 新建队伍,非 null = 加入已有队伍)
private Long activityId;
private String goodsId;
private String source;
private String channel;
private String outTradeNo; // 外部幂等键
private NotifyConfigVO notifyConfigVO; // 回调配置
// 便捷方法:设置 HTTP 回调
public void setNotifyUrl(String url) {
NotifyConfigVO vo = new NotifyConfigVO();
vo.setNotifyType("HTTP");
vo.setNotifyUrl(url);
this.notifyConfigVO = vo;
}
// 便捷方法:设置 MQ 回调
public void setNotifyMQ() {
NotifyConfigVO vo = new NotifyConfigVO();
vo.setNotifyType("MQ");
this.notifyConfigVO = vo;
}
// 内嵌类:回调配置
@Data
public static class NotifyConfigVO {
private String notifyType; // "HTTP" / "MQ"
private String notifyMQ; // MQ topic 名称
private String notifyUrl; // HTTP 回调地址
}
}
1. teamId 为空语义
teamId 为空表示"首次开团"(新建队伍),非空表示"加入已有团队"。这个设计在 domain 的 TeamStockOccupyRuleFilter 中也有对应:
// teamId 为空 → 首次开团 → 不做库存限制
if (StringUtils.isBlank(teamId)) {
return ...;
}
2. NotifyConfigVO 便捷方法
setNotifyUrl(url) 和 setNotifyMQ() 两个便捷方法封装了 NotifyConfigVO 的构建过程。调用方不需要知道 NotifyConfigVO 内部结构,只需:
dto.setNotifyUrl("https://...")dto.setNotifyMQ()3. outTradeNo 幂等键
外部调用方传入自己的交易单号,系统用它来防止重复下单。对应 domain 的 OutTradeNoRuleFilter 和 UniqueRefundNodeFilter 中的幂等校验。
┌─────────────────────────────────────────────────────────────────────┐
│ API 层 │
│ │
│ IMarketIndexService.queryGroupBuyMarketConfig() │
│ │ │
│ ├── IMarketTradeService.lockMarketPayOrder() │
│ ├── IMarketTradeService.settlementMarketPayOrder() │
│ ├── IMarketTradeService.refundMarketPayOrder() │
│ └── IDCCService.updateConfig() │
│ │
└─────────────┬───────────────────────────────────────────────────────┘
│ app 层实现时调用
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Domain 层 │
│ │
│ IIndexGroupBuyMarketService │
│ ├── indexMarketTrial() ← GoodsMarketRequestDTO │
│ ├── queryInProgressUserGroup...() ← GoodsMarketResponseDTO.teamList │
│ └── queryTeamStatistic() ← GoodsMarketResponseDTO.teamStatistic │
│ │
│ ITradeLockOrderService │
│ └── lockMarketPayOrder() ← LockMarketPayOrderRequestDTO │
│ │
│ ITradeSettlementOrderService │
│ └── settlementMarketPayOrder() ← SettlementMarketPayOrderRequestDTO │
│ │
│ ITradeRefundOrderService │
│ └── refundOrder() ← RefundMarketPayOrderRequestDTO │
│ │
└─────────────────────────────────────────────────────────────────────┘
重要规律:
| 模式 | 应用位置 | 分析 |
|---|---|---|
| 接口隔离原则 | 3 个独立的 Service 接口 | 首页查、交易、配置三者分离,而不是一个大接口 |
| DTO 模式 | 所有 Request/Response DTO | 不暴露 domain Entity 到外部,控制数据粒度 |
| 泛型包装 | Response<T> | 统一响应格式 + 类型安全 |
| Builder 模式 | 所有 Response DTO 都有 @Builder | 方便 app 层组装复杂对象 |
| 静态工厂方法 | GoodsMarketResponseDTO.Team.differenceDateTime2Str() | 日期转倒计时,内聚在数据类里 |
| 便捷方法 | LockMarketPayOrderRequestDTO.setNotifyUrl() / setNotifyMQ() | 隐藏子对象构建细节 |
以 "用户在商品详情页看到拼团价并参与拼团" 为例:
1. 前端请求商品详情
│
▼
2. trigger → IMarketIndexService.queryGroupBuyMarketConfig(requestDTO)
│ (app 层实现)
│
├── 2a. requestDTO → MarketProductEntity
│ → IIndexGroupBuyMarketService.indexMarketTrial(entity)
│ → TrialBalanceEntity (折扣价、可见性)
│ → GoodsMarketResponseDTO.goods
│
├── 2b. IIndexGroupBuyMarketService.queryInProgressUserGroupBuyOrderDetailList()
│ → List<UserGroupBuyOrderDetailEntity>
│ → GoodsMarketResponseDTO.teamList[]
│
└── 2c. IIndexGroupBuyMarketService.queryTeamStatisticByActivityId()
→ TeamStatisticVO
→ GoodsMarketResponseDTO.teamStatistic
│
▼
3. 返回 Response<GoodsMarketResponseDTO>
{
"code": "0000",
"info": "成功",
"data": {
"goods": { "goodsId": "...", "payPrice": 79.00, ... },
"teamList": [ { "userId": "小明", "teamId": "...", ... } ],
"teamStatistic": { "allTeamCount": 15, ... }
}
}
4. 用户点击"参与拼团" → 前端调锁单接口
│
▼
5. trigger → IMarketTradeService.lockMarketPayOrder(requestDTO)
│ (app 层实现)
│
├── 5a. requestDTO → UserEntity、PayActivityEntity、PayDiscountEntity
├── 5b. ITradeLockOrderService.lockMarketPayOrder(...)
│ → 责任链校验 → 写入 DB → MarketPayOrderEntity
└── 5c. MarketPayOrderEntity → LockMarketPayOrderResponseDTO
6. 用户完成支付 → 前端调结算接口
│
▼
7. trigger → IMarketTradeService.settlementMarketPayOrder(requestDTO)
│
└── ITradeSettlementOrderService.settlementMarketPayOrder(...)
→ 责任链校验 → 更新拼团状态 → 异步发送通知
这是 DDD 中最"薄"的模块——全是接口和 DTO,没有一行业务逻辑。它的唯一目的是让调用方知道怎么调、返回值长什么样。
| DTO (api 层) | Entity (domain 层) | |
|---|---|---|
| 目的 | 网络传输 | 业务建模 |
| 字段类型 | 基本类型(String, Long, BigDecimal) | 可以用枚举、值对象 |
| 包含行为 | 极少(最多一个格式化方法) | 可以有业务方法 |
| 依赖 | 无外部依赖 | 可以依赖同模块的其他类 |
如果 LockMarketPayOrderResponseDTO.tradeOrderStatus 用了 TradeOrderStatusEnumVO:
group-buy-market-domain 依赖所以 DTO 用 Integer tradeOrderStatus(原始类型),与 TradeOrderStatusEnumVO.code 对应。
看 domain 层 EndNode.doApply() 的返回:
return TrialBalanceEntity.builder()
.goodsId(skuVO.getGoodsId())
.payPrice(payPrice)
.isVisible(true)
.build();
app 层组装 GoodsMarketResponseDTO 也是类似的场景——从多个 domain 返回值中提取字段,拼装成一个 DTO。Builder 模式让这个拼装过程代码清晰可读。
学完 api 模块后,推荐按以下顺序继续:
api 是"约定",app 是"编排",trigger 是"入口",infrastructure 是"落地"。
下一篇建议阅读:
group-buy-market-app模块 —— 了解 api 定义的接口如何被实现,如何在 domain 的各服务之间编排调用。
知识笔记会随着实践和认知变化持续更新,不代表最终结论。